diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..ee4c391da300adaaa94814b165e94234915bc2a1
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+  "presets": [
+    ["latest", { "es2015": { "modules": false } }],
+    "stage-2"
+  ],
+  "env": {
+    "coverage": {
+      "plugins": [
+        ["istanbul", {
+          "exclude": [
+            "app/assets/javascripts/droplab/**/*",
+            "spec/javascripts/**/*"
+          ]
+        }],
+        ["transform-define", {
+          "process.env.BABEL_ENV": "coverage"
+        }]
+      ]
+    }
+  }
+}
diff --git a/.eslintrc b/.eslintrc
index 0fcd866778f1253b320ecb2b7a8a460405c95c26..b0ae2a319199a8e36427198e95656198fca6480a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -23,7 +23,7 @@
     }
   },
   "rules": {
-    "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+    "filenames/match-regex": [2, "^[a-z0-9_]+$"],
     "no-multiple-empty-lines": ["error", { "max": 1 }]
   }
 }
diff --git a/.flayignore b/.flayignore
index fc64b0b58924c2822036a564f2828fab909eb746..47597025115c9cd540035b84fe5a859c12c92287 100644
--- a/.flayignore
+++ b/.flayignore
@@ -2,3 +2,4 @@
 lib/gitlab/sanitizers/svg/whitelist.rb
 lib/gitlab/diff/position_tracer.rb
 app/policies/project_policy.rb
+app/models/concerns/relative_positioning.rb
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 70cce05d2b561714d9fcbcf7785f4638747157fb..0000000000000000000000000000000000000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.js.es6 gitlab-language=javascript
diff --git a/.gitignore b/.gitignore
index 5e9f19d831947e94fac7a4d25061b0fdf0160e41..51b4d06b01b2aa782c20c899550e58c7c18390fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ eslint-report.html
 /builds/*
 /shared/*
 /.gitlab_workhorse_secret
+/webpack-report/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 433b3119fbaa922a1c23c6220abd0af74d193661..34c10b3b77fb95992623558422c332fbf5683325 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,8 +7,6 @@ cache:
 
 variables:
   MYSQL_ALLOW_EMPTY_PASSWORD: "1"
-  # retry tests only in CI environment
-  RSPEC_RETRY_RETRY_COUNT: "3"
   RAILS_ENV: "test"
   SIMPLECOV: "true"
   SETUP_DB: "true"
@@ -60,7 +58,7 @@ stages:
   <<: *dedicated-runner
   <<: *use-db
   script:
-    - JOB_NAME=( $CI_BUILD_NAME )
+    - JOB_NAME=( $CI_JOB_NAME )
     - export CI_NODE_INDEX=${JOB_NAME[1]}
     - export CI_NODE_TOTAL=${JOB_NAME[2]}
     - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -69,16 +67,18 @@ stages:
     - knapsack rspec "--color --format documentation"
   artifacts:
     expire_in: 31d
+    when: always
     paths:
-    - knapsack/
     - coverage/
+    - knapsack/
+    - tmp/capybara/
 
 .spinach-knapsack: &spinach-knapsack
   stage: test
   <<: *dedicated-runner
   <<: *use-db
   script:
-    - JOB_NAME=( $CI_BUILD_NAME )
+    - JOB_NAME=( $CI_JOB_NAME )
     - export CI_NODE_INDEX=${JOB_NAME[1]}
     - export CI_NODE_TOTAL=${JOB_NAME[2]}
     - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -87,9 +87,11 @@ stages:
     - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
   artifacts:
     expire_in: 31d
+    when: always
     paths:
-    - knapsack/
     - coverage/
+    - knapsack/
+    - tmp/capybara/
 
 # Prepare and merge knapsack tests
 
@@ -178,7 +180,7 @@ spinach 9 10: *spinach-knapsack
   <<: *dedicated-runner
   stage: test
   script:
-    - bundle exec $CI_BUILD_NAME
+    - bundle exec $CI_JOB_NAME
 
 rubocop:
   <<: *ruby-static-analysis
@@ -209,7 +211,7 @@ rake ee_compat_check:
       - ee_compat_check/repo/
       - vendor/ruby
   artifacts:
-    name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+    name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
     when: on_failure
     expire_in: 10d
     paths:
@@ -222,6 +224,14 @@ rake db:migrate:reset:
   script:
     - bundle exec rake db:migrate:reset
 
+rake db:rollback:
+  stage: test
+  <<: *use-db
+  <<: *dedicated-runner
+  script:
+    - bundle exec rake db:rollback STEP=120
+    - bundle exec rake db:migrate
+
 rake db:seed_fu:
   stage: test
   <<: *use-db
@@ -240,6 +250,25 @@ rake db:seed_fu:
     paths:
       - log/development.log
 
+rake gitlab:assets:compile:
+  stage: test
+  <<: *dedicated-runner
+  dependencies: []
+  variables:
+    NODE_ENV: "production"
+    RAILS_ENV: "production"
+    SETUP_DB: "false"
+    USE_DB: "false"
+    SKIP_STORAGE_VALIDATION: "true"
+    WEBPACK_REPORT: "true"
+  script:
+    - bundle exec rake yarn:install gitlab:assets:compile
+  artifacts:
+    name: webpack-report
+    expire_in: 31d
+    paths:
+    - webpack-report/
+
 rake karma:
   cache:
     paths:
@@ -248,6 +277,8 @@ rake karma:
   stage: test
   <<: *use-db
   <<: *dedicated-runner
+  variables:
+    BABEL_ENV: "coverage"
   script:
     - bundle exec rake karma
   artifacts:
@@ -281,7 +312,7 @@ bundler:audit:
     - master@gitlab/gitlabhq
     - master@gitlab/gitlab-ee
   script:
-    - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+    - "bundle exec bundle-audit check --update"
 
 migration paths:
   stage: test
@@ -301,7 +332,7 @@ migration paths:
     - sed -i 's/localhost/redis/g' config/resque.yml
     - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
     - bundle exec rake db:drop db:create db:schema:load db:seed_fu
-    - git checkout $CI_BUILD_REF
+    - git checkout $CI_COMMIT_SHA
     - source scripts/prepare_build.sh
     - bundle exec rake db:migrate
 
@@ -339,7 +370,7 @@ lint:javascript:report:
   stage: post-test
   before_script: []
   script:
-    - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
+    - find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
     - yarn run eslint-report || true # ignore exit code
   artifacts:
     name: eslint-report
@@ -360,12 +391,13 @@ trigger_docs:
   cache: {}
   artifacts: {}
   script:
-    - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
+    - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
+    - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
   only:
     - master@gitlab-org/gitlab-ce
+    - master@gitlab-org/gitlab-ee
 
 # Notify slack in the end
-
 notify:slack:
   stage: post-test
   <<: *dedicated-runner
@@ -373,7 +405,7 @@ notify:slack:
     SETUP_DB: "false"
     USE_BUNDLE_INSTALL: "false"
   script:
-    - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
+    - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
   when: on_failure
   only:
     - master@gitlab-org/gitlab-ce
@@ -388,6 +420,7 @@ pages:
   dependencies:
     - coverage
     - rake karma
+    - rake gitlab:assets:compile
     - lint:javascript:report
   script:
     - mv public/ .public/
@@ -395,11 +428,13 @@ pages:
     - mv coverage/ public/coverage-ruby/ || true
     - mv coverage-javascript/ public/coverage-javascript/ || true
     - mv eslint-report.html public/ || true
+    - mv webpack-report/ public/webpack-report/ || true
   artifacts:
     paths:
       - public
   only:
     - master@gitlab-org/gitlab-ce
+    - master@gitlab-org/gitlab-ee
 
 # Insurance in case a gem needed by one of our releases gets yanked from
 # rubygems.org in the future.
@@ -416,3 +451,4 @@ cache gems:
       - vendor/cache
   only:
     - master@gitlab-org/gitlab-ce
+    - master@gitlab-org/gitlab-ee
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index ea895ee627519718e6e077f0808e11910f55ab69..2636010e2fb3bca23947aa890891d10c40348f64 100644
--- a/.gitlab/issue_templates/Feature Proposal.md	
+++ b/.gitlab/issue_templates/Feature Proposal.md	
@@ -5,3 +5,13 @@
 ### Proposal
 
 ### Links / references
+
+### Documentation blurb
+
+(Write the start of the documentation of this feature here, include:
+
+1. Why should someone use it; what's the underlying problem.
+2. What is the solution.
+3. How does someone use this
+
+During implementation, this can then be copied and used as a starter for the documentation.)
diff --git a/.rubocop.yml b/.rubocop.yml
index a836b469cc7f734a22e9d03c0f7df5d5d9a9b111..fa1370ea1f3d1e6d6953b2b71a3bc557307d5e35 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -22,14 +22,12 @@ AllCops:
     - 'db/fixtures/**/*'
     - 'tmp/**/*'
     - 'bin/**/*'
-    - 'lib/backup/**/*'
-    - 'lib/ci/backup/**/*'
-    - 'lib/tasks/**/*'
-    - 'lib/ci/migrate/**/*'
-    - 'lib/email_validator.rb'
-    - 'lib/gitlab/upgrader.rb'
-    - 'lib/gitlab/seeder.rb'
     - 'generator_templates/**/*'
+    - 'builds/**/*'
+
+# Gems in consecutive lines should be alphabetically sorted
+Bundler/OrderedGems:
+  Enabled: false
 
 # Style #######################################################################
 
@@ -54,6 +52,11 @@ Style/AlignArray:
 Style/AlignHash:
   Enabled: true
 
+# Here we check if the parameters on a multi-line method call or
+# definition are aligned.
+Style/AlignParameters:
+  Enabled: false
+
 # Whether `and` and `or` are banned only in conditionals (conditionals)
 # or completely (always).
 Style/AndOr:
@@ -83,15 +86,24 @@ Style/BeginBlock:
 Style/BlockComments:
   Enabled: true
 
-# Put end statement of multiline block on its own line.
-Style/BlockEndNewline:
-  Enabled: true
-
 # Avoid using {...} for multi-line blocks (multiline chaining is # always
 # ugly). Prefer {...} over do...end for single-line blocks.
 Style/BlockDelimiters:
   Enabled: true
 
+# Put end statement of multiline block on its own line.
+Style/BlockEndNewline:
+  Enabled: true
+
+ # This cop checks for braces around the last parameter in a method call
+# if the last parameter is a hash.
+Style/BracesAroundHashParameters:
+  Enabled: false
+
+# This cop checks for uses of the case equality operator(===).
+Style/CaseEquality:
+  Enabled: false
+
 # Indentation of when in a case/when/[else/]end.
 Style/CaseIndentation:
   Enabled: true
@@ -110,7 +122,7 @@ Style/ClassAndModuleChildren:
 
 # Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
 Style/ClassCheck:
-  Enabled: false
+  Enabled: true
 
 # Use self when defining module/class methods.
 Style/ClassMethods:
@@ -120,10 +132,26 @@ Style/ClassMethods:
 Style/ClassVars:
   Enabled: true
 
+# This cop checks for methods invoked via the :: operator instead
+# of the . operator (like FileUtils::rmdir instead of FileUtils.rmdir).
+Style/ColonMethodCall:
+  Enabled: true
+
+# This cop checks that comment annotation keywords are written according
+# to guidelines.
+Style/CommentAnnotation:
+  Enabled: false
+
 # Indentation of comments.
 Style/CommentIndentation:
   Enabled: true
 
+# Check for `if` and `case` statements where each branch is used for
+# assignment to the same variable when using the return of the
+# condition can be used instead.
+Style/ConditionalAssignment:
+  Enabled: true
+
 # Constants should use SCREAMING_SNAKE_CASE.
 Style/ConstantName:
   Enabled: true
@@ -136,13 +164,19 @@ Style/DefWithParentheses:
 Style/Documentation:
   Enabled: false
 
+# This cop checks for uses of double negation (!!) to convert something
+# to a boolean value. As this is both cryptic and usually redundant, it
+# should be avoided.
+Style/DoubleNegation:
+  Enabled: false
+
 # Align elses and elsifs correctly.
 Style/ElseAlignment:
   Enabled: true
 
 # Use empty lines between defs.
 Style/EmptyLineBetweenDefs:
-  Enabled: false
+  Enabled: true
 
 # Don't use several empty lines in a row.
 Style/EmptyLines:
@@ -160,14 +194,14 @@ Style/EmptyLinesAroundBlockBody:
 Style/EmptyLinesAroundClassBody:
   Enabled: true
 
-# Keeps track of empty lines around module bodies.
-Style/EmptyLinesAroundModuleBody:
-  Enabled: true
-
 # Keeps track of empty lines around method bodies.
 Style/EmptyLinesAroundMethodBody:
   Enabled: true
 
+# Keeps track of empty lines around module bodies.
+Style/EmptyLinesAroundModuleBody:
+  Enabled: true
+
 # Avoid the use of END blocks.
 Style/EndBlock:
   Enabled: true
@@ -200,24 +234,28 @@ Style/For:
 # Checks if there is a magic comment to enforce string literals
 Style/FrozenStringLiteralComment:
   Enabled: false
+
 # Do not introduce global variables.
 Style/GlobalVars:
   Enabled: true
+  Exclude:
+    - 'lib/backup/**/*'
+    - 'lib/tasks/**/*'
 
 # Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
 # over 1.8 syntax `{ :a => 1, :b => 2 }`.
 Style/HashSyntax:
   Enabled: true
 
-# Do not use if x; .... Use the ternary operator instead.
-Style/IfWithSemicolon:
-  Enabled: true
-
 # Checks that conditional statements do not have an identical line at the
 # end of each branch, which can validly be moved out of the conditional.
 Style/IdenticalConditionalBranches:
   Enabled: true
 
+# Do not use if x; .... Use the ternary operator instead.
+Style/IfWithSemicolon:
+  Enabled: true
+
 # Checks the indentation of the first line of the right-hand-side of a
 # multi-line assignment.
 Style/IndentAssignment:
@@ -258,7 +296,7 @@ Style/ModuleFunction:
 # Checks that the closing brace in an array literal is either on the same line
 # as the last array element, or a new line.
 Style/MultilineArrayBraceLayout:
-  Enabled: false
+  Enabled: true
   EnforcedStyle: symmetrical
 
 # Avoid multi-line chains of blocks.
@@ -272,7 +310,7 @@ Style/MultilineBlockLayout:
 # Checks that the closing brace in a hash literal is either on the same line as
 # the last hash element, or a new line.
 Style/MultilineHashBraceLayout:
-  Enabled: false
+  Enabled: true
   EnforcedStyle: symmetrical
 
 # Do not use then for multi-line if/unless.
@@ -304,6 +342,14 @@ Style/MultilineOperationIndentation:
 Style/MultilineTernaryOperator:
   Enabled: true
 
+# This cop checks whether some constant value isn't a
+# mutable literal (e.g. array or hash).
+Style/MutableConstant:
+  Enabled: true
+  Exclude:
+    - 'db/migrate/**/*'
+    - 'db/post_migrate/**/*'
+
 # Favor unless over if for negative conditions (or control flow or).
 Style/NegatedIf:
   Enabled: true
@@ -406,6 +452,10 @@ Style/SpaceBeforeComment:
 Style/SpaceBeforeSemicolon:
   Enabled: true
 
+# Checks for spaces inside square brackets.
+Style/SpaceInsideBrackets:
+  Enabled: true
+
 # Use spaces inside hash literal braces - or don't.
 Style/SpaceInsideHashLiteralBraces:
   Enabled: true
@@ -442,6 +492,10 @@ Style/Tab:
 Style/TrailingBlankLines:
   Enabled: true
 
+# This cop checks for trailing comma in array and hash literals.
+Style/TrailingCommaInLiteral:
+  Enabled: false
+
 # Checks for %W when interpolation is not needed.
 Style/UnneededCapitalW:
   Enabled: true
@@ -477,7 +531,7 @@ Style/WhileUntilModifier:
 
 # Use %w or %W for arrays of words.
 Style/WordArray:
-  Enabled: false
+  Enabled: true
 
 # Metrics #####################################################################
 
@@ -487,6 +541,10 @@ Metrics/AbcSize:
   Enabled: true
   Max: 60
 
+# This cop checks if the length of a block exceeds some maximum value.
+Metrics/BlockLength:
+  Enabled: false
+
 # Avoid excessive block nesting.
 Metrics/BlockNesting:
   Enabled: true
@@ -526,20 +584,21 @@ Metrics/PerceivedComplexity:
 
 # Lint ########################################################################
 
-# Checks for useless access modifiers.
-Lint/UselessAccessModifier:
-  Enabled: true
-
-# Checks for attempts to use `private` or `protected` to set the visibility
-# of a class method, which does not work.
-Lint/IneffectiveAccessModifier:
-  Enabled: false
-
 # Checks for ambiguous operators in the first argument of a method invocation
 # without parentheses.
 Lint/AmbiguousOperator:
   Enabled: true
 
+# This cop checks for ambiguous regexp literals in the first argument of
+# a method invocation without parentheses.
+Lint/AmbiguousRegexpLiteral:
+  Enabled: false
+
+# This cop checks for assignments in the conditions of
+# if/while/until.
+Lint/AssignmentInCondition:
+  Enabled: false
+
 # Align block ends correctly.
 Lint/BlockAlignment:
   Enabled: true
@@ -593,10 +652,6 @@ Lint/EndInMethod:
 Lint/EnsureReturn:
   Enabled: true
 
-# The use of eval represents a serious security risk.
-Lint/Eval:
-  Enabled: true
-
 # Catches floating-point literals too large or small for Ruby to represent.
 Lint/FloatOutOfRange:
   Enabled: true
@@ -605,11 +660,20 @@ Lint/FloatOutOfRange:
 Lint/FormatParameterMismatch:
   Enabled: true
 
+# This cop checks for *rescue* blocks with no body.
+Lint/HandleExceptions:
+  Enabled: false
+
 # Checks for adjacent string literals on the same line, which could better be
 # represented as a single string literal.
 Lint/ImplicitStringConcatenation:
   Enabled: true
 
+# Checks for attempts to use `private` or `protected` to set the visibility
+# of a class method, which does not work.
+Lint/IneffectiveAccessModifier:
+  Enabled: false
+
 # Checks for invalid character literals with a non-escaped whitespace
 # character.
 Lint/InvalidCharacterLiteral:
@@ -623,6 +687,10 @@ Lint/LiteralInCondition:
 Lint/LiteralInInterpolation:
   Enabled: true
 
+# This cop checks for uses of *begin...end while/until something*.
+Lint/Loop:
+  Enabled: false
+
 # Do not use nested method definitions.
 Lint/NestedMethodDefinition:
   Enabled: true
@@ -652,6 +720,11 @@ Lint/RescueException:
 Lint/ShadowedException:
   Enabled: false
 
+# This cop looks for use of the same name as outer local variables
+# for block arguments or block local variables.
+Lint/ShadowingOuterLocalVariable:
+  Enabled: false
+
 # Checks for Object#to_s usage in string interpolation.
 Lint/StringConversionInInterpolation:
   Enabled: true
@@ -660,16 +733,36 @@ Lint/StringConversionInInterpolation:
 Lint/UnderscorePrefixedVariableName:
   Enabled: true
 
+# This cop checks for using Fixnum or Bignum constant
+Lint/UnifiedInteger:
+  Enabled: true
+
 # Checks for rubocop:disable comments that can be removed.
 # Note: this cop is not disabled when disabling all cops.
 # It must be explicitly disabled.
 Lint/UnneededDisable:
   Enabled: false
 
+# This cop checks for unneeded usages of splat expansion
+Lint/UnneededSplatExpansion:
+  Enabled: false
+
 # Unreachable code.
 Lint/UnreachableCode:
   Enabled: true
 
+# This cop checks for unused block arguments.
+Lint/UnusedBlockArgument:
+  Enabled: false
+
+# This cop checks for unused method arguments.
+Lint/UnusedMethodArgument:
+  Enabled: false
+
+# Checks for useless access modifiers.
+Lint/UselessAccessModifier:
+  Enabled: true
+
 # Checks for useless assignment to a local variable.
 Lint/UselessAssignment:
   Enabled: true
@@ -709,6 +802,22 @@ Performance/LstripRstrip:
 Performance/RangeInclude:
   Enabled: true
 
+# This cop identifies the use of a `&block` parameter and `block.call`
+# where `yield` would do just as well.
+Performance/RedundantBlockCall:
+  Enabled: true
+
+# This cop identifies use of `Regexp#match` or `String#match in a context
+# where the integral return value of `=~` would do just as well.
+Performance/RedundantMatch:
+  Enabled: true
+
+# This cop identifies places where `Hash#merge!` can be replaced by
+# `Hash#[]=`.
+Performance/RedundantMerge:
+  Enabled: true
+  MaxKeyValuePairs: 1
+
 # Use `sort` instead of `sort_by { |x| x }`.
 Performance/RedundantSortBy:
   Enabled: true
@@ -728,6 +837,17 @@ Performance/StringReplacement:
 Performance/TimesMap:
   Enabled: true
 
+# Security ####################################################################
+
+# This cop checks for the use of JSON class methods which have potential
+# security issues.
+Security/JSONLoad:
+  Enabled: true
+
+# This cop checks for the use of *Kernel#eval*.
+Security/Eval:
+  Enabled: true
+
 # Rails #######################################################################
 
 # Enables Rails cops.
@@ -746,8 +866,19 @@ Rails/Date:
 
 # Prefer delegate method for delegations.
 Rails/Delegate:
+  Enabled: true
+
+# This cop checks dynamic `find_by_*` methods.
+Rails/DynamicFindBy:
   Enabled: false
 
+# This cop enforces that 'exit' calls are not used within a rails app.
+Rails/Exit:
+  Enabled: true
+  Exclude:
+    - lib/gitlab/upgrader.rb
+    - 'lib/backup/**/*'
+
 # Prefer `find_by` over `where.first`.
 Rails/FindBy:
   Enabled: true
@@ -760,9 +891,25 @@ Rails/FindEach:
 Rails/HasAndBelongsToMany:
   Enabled: true
 
+# This cop is used to identify usages of http methods like `get`, `post`,
+# `put`, `patch` without the usage of keyword arguments in your tests and
+# change them to use keyword args.
+Rails/HttpPositionalArguments:
+  Enabled: false
+
 # Checks for calls to puts, print, etc.
 Rails/Output:
   Enabled: true
+  Exclude:
+    - lib/gitlab/seeder.rb
+    - lib/gitlab/upgrader.rb
+    - 'lib/backup/**/*'
+    - 'lib/tasks/**/*'
+
+# This cop checks for the use of output safety calls like html_safe and
+# raw.
+Rails/OutputSafety:
+  Enabled: false
 
 # Checks for incorrect grammar when using methods like `3.day.ago`.
 Rails/PluralizationGrammar:
@@ -776,6 +923,14 @@ Rails/ReadWriteAttribute:
 Rails/ScopeArgs:
   Enabled: true
 
+# This cop checks for the use of Time methods without zone.
+Rails/TimeZone:
+  Enabled: false
+
+# This cop checks for the use of old-style attribute validation macros.
+Rails/Validation:
+  Enabled: true
+
 # RSpec #######################################################################
 
 # Check that instances are not being stubbed globally.
@@ -784,7 +939,7 @@ RSpec/AnyInstance:
 
 # Check for expectations where `be(...)` can replace `eql(...)`.
 RSpec/BeEql:
-  Enabled: false
+  Enabled: true
 
 # Check that the first argument to the top level describe is the tested class or
 # module.
@@ -833,21 +988,51 @@ RSpec/Focus:
 RSpec/InstanceVariable:
   Enabled: false
 
+# Checks for `subject` definitions that come after `let` definitions.
+RSpec/LeadingSubject:
+  Enabled: false
+
+# Checks unreferenced `let!` calls being used for test setup.
+RSpec/LetSetup:
+  Enabled: false
+
+# Check that chains of messages are not being stubbed.
+RSpec/MessageChain:
+  Enabled: false
+
+# Checks that message expectations are set using spies.
+RSpec/MessageSpies:
+  Enabled: false
+
 # Checks for multiple top-level describes.
 RSpec/MultipleDescribes:
   Enabled: false
 
+# Checks if examples contain too many `expect` calls.
+RSpec/MultipleExpectations:
+  Enabled: false
+
+# Checks for explicitly referenced test subjects.
+RSpec/NamedSubject:
+  Enabled: false
+
+# Checks for nested example groups.
+RSpec/NestedGroups:
+  Enabled: false
+
 # Enforces the usage of the same method on all negative message expectations.
 RSpec/NotToNot:
   EnforcedStyle: not_to
   Enabled: true
 
-# Prefer using verifying doubles over normal doubles.
-RSpec/VerifiedDoubles:
+# Check for repeated description strings in example groups.
+RSpec/RepeatedDescription:
   Enabled: false
 
-# Custom ######################################################################
+# Checks for stubbed test subjects.
+RSpec/SubjectStub:
+  Enabled: false
 
-# Disallow the `git` and `github` arguments in the Gemfile.
-GemFetcher:
-  Enabled: true
+# Prefer using verifying doubles over normal doubles.
+RSpec/VerifiedDoubles:
+  Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index a5b4d2f5b02cc96589b8b9a6fd70a0ee3014f8a1..c24142c0a111e4df09e65bc22004fe42a61da66d 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,79 +1,13 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config --exclude-limit 0`
-# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0.
+# on 2017-02-22 13:02:35 -0600 using RuboCop version 0.47.1.
 # The point is for the user to remove these configuration records
 # one by one as the offenses are removed from the code base.
 # Note that changes in the inspected code, or installation of new
 # versions of RuboCop, may require this file to be generated again.
 
-# Offense count: 27
-# Configuration parameters: Include.
-# Include: **/Gemfile, **/gems.rb
-Bundler/OrderedGems:
-  Enabled: false
-
-# Offense count: 175
-Lint/AmbiguousRegexpLiteral:
-  Enabled: false
-
-# Offense count: 53
-# Configuration parameters: AllowSafeAssignment.
-Lint/AssignmentInCondition:
-  Enabled: false
-
-# Offense count: 20
-Lint/HandleExceptions:
-  Enabled: false
-
-# Offense count: 1
-Lint/Loop:
-  Enabled: false
-
-# Offense count: 27
-Lint/ShadowingOuterLocalVariable:
-  Enabled: false
-
-# Offense count: 10
-# Cop supports --auto-correct.
-Lint/UnifiedInteger:
-  Enabled: false
-
-# Offense count: 21
-# Cop supports --auto-correct.
-Lint/UnneededSplatExpansion:
-  Enabled: false
-
-# Offense count: 82
-# Cop supports --auto-correct.
-# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
-Lint/UnusedBlockArgument:
-  Enabled: false
-
-# Offense count: 173
-# Cop supports --auto-correct.
-# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
-Lint/UnusedMethodArgument:
-  Enabled: false
-
-# Offense count: 93
-# Configuration parameters: CountComments.
-Metrics/BlockLength:
-  Enabled: false
-
-# Offense count: 3
-# Cop supports --auto-correct.
-Performance/RedundantBlockCall:
-  Enabled: false
-
-# Offense count: 5
-# Cop supports --auto-correct.
-Performance/RedundantMatch:
-  Enabled: false
-
-# Offense count: 32
-# Cop supports --auto-correct.
-# Configuration parameters: MaxKeyValuePairs.
-Performance/RedundantMerge:
+# Offense count: 51
+RSpec/BeforeAfterAll:
   Enabled: false
 
 # Offense count: 15
@@ -81,7 +15,11 @@ Performance/RedundantMerge:
 RSpec/EmptyExampleGroup:
   Enabled: false
 
-# Offense count: 58
+# Offense count: 1
+RSpec/ExpectOutput:
+  Enabled: false
+
+# Offense count: 63
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: implicit, each, example
 RSpec/HookArgument:
@@ -93,147 +31,59 @@ RSpec/HookArgument:
 RSpec/ImplicitExpect:
   Enabled: false
 
-# Offense count: 237
-RSpec/LeadingSubject:
-  Enabled: false
-
-# Offense count: 253
-RSpec/LetSetup:
-  Enabled: false
-
-# Offense count: 13
-RSpec/MessageChain:
-  Enabled: false
-
-# Offense count: 479
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: have_received, receive
-RSpec/MessageSpies:
-  Enabled: false
-
-# Offense count: 3036
-RSpec/MultipleExpectations:
-  Enabled: false
-
-# Offense count: 2133
-RSpec/NamedSubject:
-  Enabled: false
-
-# Offense count: 1974
-# Configuration parameters: MaxNesting.
-RSpec/NestedGroups:
+# Offense count: 36
+RSpec/RepeatedExample:
   Enabled: false
 
-# Offense count: 32
-RSpec/RepeatedDescription:
+# Offense count: 34
+RSpec/ScatteredSetup:
   Enabled: false
 
 # Offense count: 1
 RSpec/SingleArgumentMessageChain:
   Enabled: false
 
-# Offense count: 133
-RSpec/SubjectStub:
-  Enabled: false
-
-# Offense count: 104
-# Cop supports --auto-correct.
-# Configuration parameters: Whitelist.
-# Whitelist: find_by_sql
-Rails/DynamicFindBy:
-  Enabled: false
-
-# Offense count: 932
-# Cop supports --auto-correct.
-# Configuration parameters: Include.
-# Include: spec/**/*, test/**/*
-Rails/HttpPositionalArguments:
-  Enabled: false
-
-# Offense count: 55
-Rails/OutputSafety:
+# Offense count: 163
+Rails/FilePath:
   Enabled: false
 
-# Offense count: 182
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: strict, flexible
-Rails/TimeZone:
-  Enabled: false
-
-# Offense count: 15
-# Cop supports --auto-correct.
+# Offense count: 2
 # Configuration parameters: Include.
-# Include: app/models/**/*.rb
-Rails/Validation:
+# Include: db/migrate/*.rb
+Rails/ReversibleMigration:
   Enabled: false
 
-# Offense count: 8
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect.
-Security/JSONLoad:
+# Offense count: 278
+# Configuration parameters: Blacklist.
+# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
+Rails/SkipsModelValidations:
   Enabled: false
 
-# Offense count: 346
+# Offense count: 7
 # Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
-# SupportedStyles: with_first_parameter, with_fixed_indentation
-Style/AlignParameters:
+Security/YAMLLoad:
   Enabled: false
 
-# Offense count: 54
+# Offense count: 55
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: percent_q, bare_percent
 Style/BarePercentLiterals:
   Enabled: false
 
-# Offense count: 358
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: braces, no_braces, context_dependent
-Style/BracesAroundHashParameters:
-  Enabled: false
-
-# Offense count: 6
-Style/CaseEquality:
-  Enabled: false
-
-# Offense count: 37
-# Cop supports --auto-correct.
-Style/ColonMethodCall:
-  Enabled: false
-
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: Keywords.
-# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
-Style/CommentAnnotation:
-  Enabled: false
-
-# Offense count: 29
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
-# SupportedStyles: assign_to_condition, assign_inside_condition
-Style/ConditionalAssignment:
-  Enabled: false
-
-# Offense count: 1210
+# Offense count: 1304
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: leading, trailing
 Style/DotPosition:
   Enabled: false
 
-# Offense count: 18
-Style/DoubleNegation:
-  Enabled: false
-
-# Offense count: 7
+# Offense count: 6
 # Cop supports --auto-correct.
 Style/EachWithObject:
   Enabled: false
 
-# Offense count: 24
+# Offense count: 25
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: empty, nil, both
@@ -245,14 +95,14 @@ Style/EmptyElse:
 Style/EmptyLiteral:
   Enabled: false
 
-# Offense count: 57
+# Offense count: 56
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: compact, expanded
 Style/EmptyMethod:
   Enabled: false
 
-# Offense count: 147
+# Offense count: 184
 # Cop supports --auto-correct.
 # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
 Style/ExtraSpacing:
@@ -264,50 +114,50 @@ Style/ExtraSpacing:
 Style/FormatString:
   Enabled: false
 
-# Offense count: 238
+# Offense count: 268
 # Configuration parameters: MinBodyLength.
 Style/GuardClause:
   Enabled: false
 
-# Offense count: 11
+# Offense count: 14
 Style/IfInsideElse:
   Enabled: false
 
-# Offense count: 173
+# Offense count: 179
 # Cop supports --auto-correct.
 # Configuration parameters: MaxLineLength.
 Style/IfUnlessModifier:
   Enabled: false
 
-# Offense count: 55
+# Offense count: 57
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
 # SupportedStyles: special_inside_parentheses, consistent, align_brackets
 Style/IndentArray:
   Enabled: false
 
-# Offense count: 101
+# Offense count: 120
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
 # SupportedStyles: special_inside_parentheses, consistent, align_braces
 Style/IndentHash:
   Enabled: false
 
-# Offense count: 41
+# Offense count: 45
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: line_count_dependent, lambda, literal
 Style/Lambda:
   Enabled: false
 
-# Offense count: 5
+# Offense count: 7
 # Cop supports --auto-correct.
 Style/LineEndConcatenation:
   Enabled: false
 
-# Offense count: 19
+# Offense count: 22
 # Cop supports --auto-correct.
-Style/MethodCallParentheses:
+Style/MethodCallWithoutArgsParentheses:
   Enabled: false
 
 # Offense count: 9
@@ -319,61 +169,49 @@ Style/MethodMissing:
 Style/MultilineIfModifier:
   Enabled: false
 
-# Offense count: 179
-# Cop supports --auto-correct.
-Style/MutableConstant:
-  Enabled: false
-
-# Offense count: 8
+# Offense count: 22
 # Cop supports --auto-correct.
 Style/NestedParenthesizedCalls:
   Enabled: false
 
-# Offense count: 13
+# Offense count: 17
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
 # SupportedStyles: skip_modifier_ifs, always
 Style/Next:
   Enabled: false
 
-# Offense count: 19
+# Offense count: 31
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
 # SupportedOctalStyles: zero_with_o, zero_only
 Style/NumericLiteralPrefix:
   Enabled: false
 
-# Offense count: 19
+# Offense count: 77
 # Cop supports --auto-correct.
 # Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
 # SupportedStyles: predicate, comparison
 Style/NumericPredicate:
   Enabled: false
 
-# Offense count: 34
+# Offense count: 36
 # Cop supports --auto-correct.
 Style/ParallelAssignment:
   Enabled: false
 
-# Offense count: 417
+# Offense count: 477
 # Cop supports --auto-correct.
 # Configuration parameters: PreferredDelimiters.
 Style/PercentLiteralDelimiters:
   Enabled: false
 
-# Offense count: 10
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: lower_case_q, upper_case_q
-Style/PercentQLiterals:
-  Enabled: false
-
-# Offense count: 13
+# Offense count: 14
 # Cop supports --auto-correct.
 Style/PerlBackrefs:
   Enabled: false
 
-# Offense count: 64
+# Offense count: 72
 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
 # NamePrefix: is_, has_, have_
 # NamePrefixBlacklist: is_, has_, have_
@@ -381,7 +219,7 @@ Style/PerlBackrefs:
 Style/PredicateName:
   Enabled: false
 
-# Offense count: 33
+# Offense count: 39
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: short, verbose
@@ -393,7 +231,7 @@ Style/PreferredHashMethods:
 Style/Proc:
   Enabled: false
 
-# Offense count: 50
+# Offense count: 62
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: compact, exploded
@@ -405,30 +243,30 @@ Style/RaiseArgs:
 Style/RedundantBegin:
   Enabled: false
 
-# Offense count: 29
+# Offense count: 32
 # Cop supports --auto-correct.
 Style/RedundantFreeze:
   Enabled: false
 
-# Offense count: 11
+# Offense count: 15
 # Cop supports --auto-correct.
 # Configuration parameters: AllowMultipleReturnValues.
 Style/RedundantReturn:
   Enabled: false
 
-# Offense count: 359
+# Offense count: 365
 # Cop supports --auto-correct.
 Style/RedundantSelf:
   Enabled: false
 
-# Offense count: 105
+# Offense count: 108
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
 # SupportedStyles: slashes, percent_r, mixed
 Style/RegexpLiteral:
   Enabled: false
 
-# Offense count: 19
+# Offense count: 22
 # Cop supports --auto-correct.
 Style/RescueModifier:
   Enabled: false
@@ -438,19 +276,13 @@ Style/RescueModifier:
 Style/SelfAssignment:
   Enabled: false
 
-# Offense count: 2
-# Configuration parameters: Methods.
-# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]}
-Style/SingleLineBlockParams:
-  Enabled: false
-
 # Offense count: 50
 # Cop supports --auto-correct.
 # Configuration parameters: AllowIfMethodIsEmpty.
 Style/SingleLineMethods:
   Enabled: false
 
-# Offense count: 138
+# Offense count: 155
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: space, no_space
@@ -463,26 +295,22 @@ Style/SpaceBeforeBlockBraces:
 Style/SpaceBeforeFirstArg:
   Enabled: false
 
-# Offense count: 37
+# Offense count: 38
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: require_no_space, require_space
 Style/SpaceInLambdaLiteral:
   Enabled: false
 
-# Offense count: 174
+# Offense count: 203
 # Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
 # SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
 Style/SpaceInsideBlockBraces:
   Enabled: false
 
-# Offense count: 115
-# Cop supports --auto-correct.
-Style/SpaceInsideBrackets:
-  Enabled: false
-
-# Offense count: 77
+# Offense count: 91
 # Cop supports --auto-correct.
 Style/SpaceInsideParens:
   Enabled: false
@@ -492,21 +320,21 @@ Style/SpaceInsideParens:
 Style/SpaceInsidePercentLiteralDelimiters:
   Enabled: false
 
-# Offense count: 53
+# Offense count: 55
 # Cop supports --auto-correct.
 # Configuration parameters: SupportedStyles.
 # SupportedStyles: use_perl_names, use_english_names
 Style/SpecialGlobalVars:
   EnforcedStyle: use_perl_names
 
-# Offense count: 25
+# Offense count: 40
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: single_quotes, double_quotes
 Style/StringLiteralsInInterpolation:
   Enabled: false
 
-# Offense count: 54
+# Offense count: 57
 # Cop supports --auto-correct.
 # Configuration parameters: IgnoredMethods.
 # IgnoredMethods: respond_to, define_method
@@ -520,27 +348,20 @@ Style/SymbolProc:
 Style/TernaryParentheses:
   Enabled: false
 
-# Offense count: 36
+# Offense count: 43
 # Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
 Style/TrailingCommaInArguments:
   Enabled: false
 
-# Offense count: 150
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
-Style/TrailingCommaInLiteral:
-  Enabled: false
-
-# Offense count: 7
+# Offense count: 13
 # Cop supports --auto-correct.
 # Configuration parameters: AllowNamedUnderscoreVariables.
 Style/TrailingUnderscoreVariable:
   Enabled: false
 
-# Offense count: 67
+# Offense count: 70
 # Cop supports --auto-correct.
 Style/TrailingWhitespace:
   Enabled: false
@@ -552,12 +373,12 @@ Style/TrailingWhitespace:
 Style/TrivialAccessors:
   Enabled: false
 
-# Offense count: 2
+# Offense count: 6
 # Cop supports --auto-correct.
 Style/UnlessElse:
   Enabled: false
 
-# Offense count: 17
+# Offense count: 22
 # Cop supports --auto-correct.
 Style/UnneededInterpolation:
   Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58b8cf2ad831991ab2ea4c8f01e723e63635c782..da1898e37707609c001cc6b38d0f30e2b8fb0072 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,230 @@
 documentation](doc/development/changelog.md) for instructions on adding your own
 entry.
 
+## 8.17.4 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
+## 8.17.3 (2017-03-07)
+
+- Fix the redirect to custom home page URL. !9518
+- Fix broken migration when upgrading straight to 8.17.1. !9613
+- Make projects dropdown only show projects you are a member of. !9614
+- Fix creating a file in an empty repo using the API. !9632
+- Don't copy tooltip when copying GFM.
+- Fix cherry-picking or reverting through an MR.
+
+## 8.17.2 (2017-03-01)
+
+- Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602
+
+## 8.17.1 (2017-02-28)
+
+- Replace setInterval with setTimeout to prevent highly frequent requests. !9271 (Takuya Noguchi)
+- Disable unused tags count cache for Projects, Builds and Runners.
+- Spam check and reCAPTCHA improvements.
+- Allow searching issues for strings containing colons.
+- Disabled tooltip on add issues button in usse boards.
+- Fixed commit search UI.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+- Disable invalid service templates.
+- Use default branch as target_branch when parameter is missing.
+- Upgrade GitLab Pages to v0.3.2.
+- Add performance query regression fix for !9088 affecting #27267.
+- Chat slash commands show labels correctly.
+
+## 8.17.0 (2017-02-22)
+
+- API: Fix file downloading. !0 (8267)
+- Changed composer installer script in the CI PHP example doc. !4342 (Jeffrey Cafferata)
+- Display fullscreen button on small screens. !5302 (winniehell)
+- Add system hook for when a project is updated (other than rename/transfer). !5711 (Tommy Beadle)
+- Fix notifications when set at group level. !6813 (Alexandre Maia)
+- Project labels can now be promoted to group labels. !7242 (Olaf Tomalka)
+- use webpack to bundle frontend assets and use karma for frontend testing. !7288
+- Adds back ability to stop all environments. !7379
+- Added labels empty state. !7443
+- Add ability to define a coverage regex in the .gitlab-ci.yml. !7447 (Leandro Camargo)
+- Disable automatic login after clicking email confirmation links. !7472
+- Search feature: redirects to commit page if query is commit sha and only commit found. !8028 (YarNayar)
+- Create a TODO for user who set auto-merge when a build fails, merge conflict occurs. !8056 (twonegatives)
+- Don't group issues by project on group-level and dashboard issue indexes. !8111 (Bernardo Castro)
+- Mark MR as WIP when pushing WIP commits. !8124 (Jurre Stender @jurre)
+- Flag multiple empty lines in eslint, fix offenses. !8137
+- Add sorting pipeline for a commit. !8319 (Takuya Noguchi)
+- Adds service trigger events to api. !8324
+- Update pipeline and commit links when CI status is updated. !8351
+- Hide version check image if there is no internet connection. !8355 (Ken Ding)
+- Prevent removal of input fields if it is the parent dropdown element. !8397
+- Introduce maximum session time for terminal websocket connection. !8413
+- Allow creating protected branches when user can merge to such branch. !8458
+- Refactor MergeRequests::BuildService. !8462 (Rydkin Maxim)
+- Added GitLab Pages to CE. !8463
+- Support notes when a project is not specified (personal snippet notes). !8468
+- Use warning icon in mini-graph if stage passed conditionally. !8503
+- Don’t count tasks that are not defined as list items correctly. !8526
+- Reformat messages ChatOps. !8528
+- Copy commit SHA to clipboard. !8547
+- Improve button accessibility on pipelines page. !8561
+- Display project ID in project settings. !8572 (winniehell)
+- PlantUML support for Markdown. !8588 (Horacio Sanson)
+- Fix reply by email without sub-addressing for some clients from Microsoft and Apple. !8620
+- Fix nested tasks in ordered list. !8626
+- Fix Sort by Recent Sign-in in Admin Area. !8637 (Poornima M)
+- Avoid repeated dashes in $CI_ENVIRONMENT_SLUG. !8638
+- Only show Merge Request button when user can create a MR. !8639
+- Prevent copying of line numbers in parallel diff view. !8706
+- Improve build policy and access abilities. !8711
+- API: Remove /projects/:id/keys/.. endpoints. !8716 (Robert Schilling)
+- API: Remove deprecated 'expires_at' from project snippets. !8723 (Robert Schilling)
+- Add `copy` backup strategy to combat file changed errors. !8728
+- adds avatar for discussion note. !8734
+- Add link verification to badge partial in order to render a badge without a link. !8740
+- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752
+- prevent diff unfolding link from appearing when there are no more lines to show. !8761
+- Redesign searchbar in admin project list. !8776
+- Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere. !8787
+- dismiss sidebar on repo buttons click. !8798 (Adam Pahlevi)
+- fixed small mini pipeline graph line glitch. !8804
+- Make all system notes lowercase. !8807
+- Support unauthenticated LFS object downloads for public projects. !8824 (Ben Boeckel)
+- Add read-only full_path and full_name attributes to Group API. !8827
+- allow relative url change without recompiling frontend assets. !8831
+- Use vue.js Pipelines table in commit and merge request view. !8844
+- Use reCaptcha when an issue is identified as a spam. !8846
+- resolve deprecation warnings. !8855 (Adam Pahlevi)
+- Cop for gem fetched from a git source. !8856 (Adam Pahlevi)
+- Remove flash warning from login page. !8864 (Gerald J. Padilla)
+- Adds documentation for how to use Vue.js. !8866
+- Add 'View on [env]' link to blobs and individual files in diffs. !8867
+- Replace word user with member. !8872
+- Change the reply shortcut to focus the field even without a selection. !8873 (Brian Hall)
+- Unify MR diff file button style. !8874
+- Unify projects search by removing /projects/:search endpoint. !8877
+- Fix disable storing of sensitive information when importing a new repo. !8885 (Bernard Pietraga)
+- Fix pipeline graph vertical spacing in Firefox and Safari. !8886
+- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891
+- Fix Ctrl+Click support for Todos and Merge Request page tabs. !8898
+- Fix wrong call to ProjectCacheWorker.perform. !8910
+- Don't perform Devise trackable updates on blocked User records. !8915
+- Add ability to export project inherited group members to Import/Export. !8923
+- replace `find_with_namespace` with `find_by_full_path`. !8949 (Adam Pahlevi)
+- Fixes flickering of avatar border in mention dropdown. !8950
+- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956
+- Fix deleting projects with pipelines and builds. !8960
+- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko)
+- Ensure autogenerated title does not cause failing spec. !8963 (brian m. carlson)
+- Update doc for enabling or disabling GitLab CI. !8965 (Takuya Noguchi)
+- Remove deprecated MR and Issue endpoints and preserve V3 namespace. !8967
+- Fixed "substract" typo on /help/user/project/slash_commands. !8976 (Jason Aquino)
+- Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context. !8981
+- use babel to transpile all non-vendor javascript assets regardless of file extension. !8988
+- Fix MR widget url. !8989
+- Fixes hover cursor on pipeline pagenation. !9003
+- Layer award emoji dropdown over the right sidebar. !9004
+- Do not display deploy keys in user's own ssh keys list. !9024
+- upgrade babel 5.8.x to babel 6.22.x. !9072
+- upgrade to webpack v2.2. !9078
+- Trigger autocomplete after selecting a slash command. !9117
+- Add space between text and loading icon in Megre Request Widget. !9119
+- Fix job to pipeline renaming. !9147
+- Replace static fixture for merge_request_tabs_spec.js. !9172 (winniehell)
+- Replace static fixture for right_sidebar_spec.js. !9211 (winniehell)
+- Show merge errors in merge request widget. !9229
+- Increase process_commit queue weight from 2 to 3. !9326 (blackst0ne)
+- Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb.
+- Force new password after password reset via API. (George Andrinopoulos)
+- Allows to search within project by commit hash. (YarNayar)
+- Show organisation membership and delete comment on smaller viewports, plus change comment author name to username.
+- Remove turbolinks.
+- Convert pipeline action icons to svg to have them propperly positioned.
+- Remove rogue scrollbars for issue comments with inline elements.
+- Align Segoe UI label text.
+- Color + and - signs in diffs to increase code legibility.
+- Fix tab index order on branch commits list page. (Ryan Harris)
+- Add hover style to copy icon on commit page header. (Ryan Harris)
+- Remove hover animation from row elements.
+- Improve pipeline status icon linking in widgets.
+- Fix commit title bar and repository view copy clipboard button order on last commit in repository view.
+- Fix mini-pipeline stage tooltip text wrapping.
+- Updated builds info link on the project settings page. (Ryan Harris)
+- 27240 Make progress bars consistent.
+- Only render hr when user can't archive project.
+- 27352-search-label-filter-header.
+- Include :author, :project, and :target in Event.with_associations.
+- Don't instantiate AR objects in Event.in_projects.
+- Don't capitalize environment name in show page.
+- Update and pin the `jwt` gem to ~> 1.5.6.
+- Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles.
+- Give ci status text on pipeline graph a better font-weight.
+- Add default labels to bulk assign dropdowns.
+- Only return target project's comments for a commit.
+- Fixes Pipelines table is not showing branch name for commit.
+- Fix regression where cmd-click stopped working for todos and merge request tabs.
+- Fix stray pipelines API request when showing MR.
+- Fix Merge request pipelines displays JSON.
+- Fix current build arrow indicator.
+- Fix contribution activity alignment.
+- Show Pipeline(not Job) in MR desktop notification.
+- Fix tooltips in mini pipeline graph.
+- Display loading indicator when filtering ref switcher dropdown.
+- Show pipeline graph in MR widget if there are any stages.
+- Fix icon colors in merge request widget mini graph.
+- Improve blockquote formatting in notification emails.
+- Adds container to tooltip in order to make it work with overflow:hidden in parent element.
+- Restore pagination to admin abuse reports.
+- Ensure export files are removed after a namespace is deleted.
+- Add `y` keyboard shortcut to move to file permalink.
+- Adds /target_branch slash command functionality for merge requests. (YarNayar)
+- Patch Asciidocs rendering to block XSS.
+- contribution calendar scrolls from right to left.
+- Copying a rendered issue/comment will paste into GFM textareas as actual GFM.
+- Don't delete assigned MRs/issues when user is deleted.
+- Remove new branch button for confidential issues.
+- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
+- Don't connect in Gitlab::Database.adapter_name.
+- Prevent users from creating notes on resources they can't access.
+- Ignore encrypted attributes in Import/Export.
+- Change rspec test to guarantee window is resized before visiting page.
+- Prevent users from deleting system deploy keys via the project deploy key API.
+- Fix XSS vulnerability in SVG attachments.
+- Make MR-review-discussions more reliable.
+- fix incorrect sidekiq concurrency count in admin background page. (wendy0402)
+- Make notification_service spec DRYer by making test reusable. (YarNayar)
+- Redirect http://someproject.git to http://someproject. (blackst0ne)
+- Fixed group label links in issue/merge request sidebar.
+- Improve gl.utils.handleLocationHash tests.
+- Fixed Issuable sidebar not closing on smaller/mobile sized screens.
+- Resets assignee dropdown when sidebar is open.
+- Disallow system notes for closed issuables.
+- Fix timezone on issue boards due date.
+- Remove unused js response from refs controller.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Fixed merge requests tab extra margin when fixed to window.
+- Patch XSS vulnerability in RDOC support.
+- Refresh authorizations when transferring projects.
+- Remove issue and MR counts from labels index.
+- Don't use backup Active Record connections for Sidekiq.
+- Add index to ci_trigger_requests for commit_id.
+- Add indices to improve loading of labels page.
+- Reduced query count for snippet search.
+- Update GitLab Pages to v0.3.1.
+- Upgrade omniauth gem to 1.3.2.
+- Remove deprecated GitlabCiService.
+- Requeue pending deletion projects.
+
+## 8.16.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
+## 8.16.7 (2017-02-27)
+
+- No changes.
+- No changes.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+
 ## 8.16.6 (2017-02-17)
 
 - API: Fix file downloading. !0 (8267)
@@ -197,6 +421,11 @@ entry.
 - Add margin to markdown math blocks.
 - Add hover state to MR comment reply button.
 
+## 8.15.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
 ## 8.15.7 (2017-02-15)
 
 - No changes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de32a953f631dbf95fdd555a47a1b3e3cb819388..a285e8ab74f98860aecc64f3fdeeecdf4c16cf77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,32 +1,48 @@
+## Contributor license agreement
+
+By submitting code as an individual you agree to the
+[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
+By submitting code as an entity you agree to the
+[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+
+_This notice should stay as the first item in the CONTRIBUTING.MD file._
+
+---
+
 <!-- START doctoc generated TOC please keep comment here to allow auto update -->
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
 
+- [Contributor license agreement](#contributor-license-agreement)
 - [Contribute to GitLab](#contribute-to-gitlab)
-    - [Contributor license agreement](#contributor-license-agreement)
-    - [Security vulnerability disclosure](#security-vulnerability-disclosure)
-    - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
-    - [Helping others](#helping-others)
-    - [I want to contribute!](#i-want-to-contribute)
-    - [Implement design & UI elements](#implement-design-ui-elements)
-    - [Issue tracker](#issue-tracker)
-        - [Feature proposals](#feature-proposals)
-        - [Issue tracker guidelines](#issue-tracker-guidelines)
-        - [Issue weight](#issue-weight)
-        - [Regression issues](#regression-issues)
-        - [Technical debt](#technical-debt)
-        - [Stewardship](#stewardship)
-    - [Merge requests](#merge-requests)
-        - [Merge request guidelines](#merge-request-guidelines)
-        - [Contribution acceptance criteria](#contribution-acceptance-criteria)
-    - [Changes for Stable Releases](#changes-for-stable-releases)
-    - [Definition of done](#definition-of-done)
-    - [Style guides](#style-guides)
-    - [Code of conduct](#code-of-conduct)
+- [Security vulnerability disclosure](#security-vulnerability-disclosure)
+- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
+- [Helping others](#helping-others)
+- [I want to contribute!](#i-want-to-contribute)
+- [Implement design & UI elements](#implement-design-ui-elements)
+- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
+    - [Retrospective](#retrospective)
+    - [Kickoff](#kickoff)
+- [Issue tracker](#issue-tracker)
+    - [Feature proposals](#feature-proposals)
+    - [Issue tracker guidelines](#issue-tracker-guidelines)
+    - [Issue weight](#issue-weight)
+    - [Regression issues](#regression-issues)
+    - [Technical debt](#technical-debt)
+    - [Stewardship](#stewardship)
+- [Merge requests](#merge-requests)
+    - [Merge request guidelines](#merge-request-guidelines)
+    - [Contribution acceptance criteria](#contribution-acceptance-criteria)
+- [Changes for Stable Releases](#changes-for-stable-releases)
+- [Definition of done](#definition-of-done)
+- [Style guides](#style-guides)
+- [Code of conduct](#code-of-conduct)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
-# Contribute to GitLab
+---
+
+## Contribute to GitLab
 
 Thank you for your interest in contributing to GitLab. This guide details how
 to contribute to GitLab in a way that is efficient for everyone.
@@ -41,13 +57,6 @@ operates please see [the GitLab contributing process](PROCESS.md).
 
 - [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
 
-## Contributor license agreement
-
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
-
 ## Security vulnerability disclosure
 
 Please report suspected security vulnerabilities in private to
@@ -69,6 +78,13 @@ towards getting your issue resolved.
 Issues and merge requests should be in English and contain appropriate language
 for audiences of all ages.
 
+If a contributor is no longer actively working on a submitted merge request
+we can decide that the merge request will be finished by one of our
+[Merge request coaches][team] or close the merge request. We make this decision
+based on how important the change is for our product vision. If a Merge request
+coach is going to finish the merge request we assign the 
+~"coach will finish" label.
+
 ## Helping others
 
 Please help other GitLab users when you can. The channels people will reach out
@@ -85,6 +101,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep
 These issues will be of reasonable size and challenge, for anyone to start
 contributing to GitLab.
 
+## Workflow labels
+
+Labelling issues is described in the [GitLab Inc engineering workflow].
+
 ## Implement design & UI elements
 
 Please see the [UX Guide for GitLab].
@@ -290,10 +310,13 @@ request is as follows:
 1. [Generate a changelog entry with `bin/changelog`][changelog]
 1. If you are writing documentation, make sure to follow the
    [documentation styleguide][doc-styleguide]
-1. If you have multiple commits please combine them into one commit by
-   [squashing them][git-squash]
+1. If you have multiple commits please combine them into a few logically
+  organized commits by [squashing them][git-squash]
 1. Push the commit(s) to your fork
 1. Submit a merge request (MR) to the `master` branch
+1. Leave the approvals settings as they are:
+  1. Your merge request needs at least 1 approval
+  1. You don't have to select any approvers
 1. The MR title should describe the change you want to make
 1. The MR description should give a motive for your change and the method you
    used to achieve it.
@@ -336,13 +359,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k
 
 For examples of feedback on merge requests please look at already
 [closed merge requests][closed-merge-requests]. If you would like quick feedback
-on your merge request feel free to mention one of the Merge Marshalls in the
-[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/).
+on your merge request feel free to mention someone from the [core team] or one
+of the [Merge request coaches][team].
 Please ensure that your merge request meets the contribution acceptance criteria.
 
 When having your code reviewed and when reviewing merge requests please take the
 [code review guidelines](doc/development/code_review.md) into account.
 
+### Getting your merge request reviewed, approved, and merged
+
+There are a few rules to get your merge request accepted:
+
+1. Your merge request should only be **merged by a [maintainer][team]**.
+  1. If your merge request includes only backend changes [^1], it must be
+    **approved by a [backend maintainer][team]**.
+  1. If your merge request includes only frontend changes [^1], it must be
+    **approved by a [frontend maintainer][team]**.
+  1. If your merge request includes frontend and backend changes [^1], it must
+    be approved by a frontend **and** a backend maintainer.
+1. To lower the amount of merge requests maintainers need to review, you can
+  ask or assign any [reviewers][team] for a first review.
+  1. If you need some guidance (e.g. it's your first merge request), feel free
+    to ask one of the [Merge request coaches][team].
+  1. The reviewer will assign the merge request to a maintainer once the
+    reviewer is satisfied with the state of the merge request.
+
 ### Contribution acceptance criteria
 
 1. The change is as small as possible
@@ -365,6 +406,12 @@ When having your code reviewed and when reviewing merge requests please take the
 1. Contains functionality we think other users will benefit from too
 1. Doesn't add configuration options or settings options since they complicate
    making and testing future changes
+1. Changes do not adversely degrade performance.
+   - Avoid repeated polling of endpoints that require a significant amount of overhead
+   - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
+   - Avoid repeated access of filesystem
+1. If you need polling to support real-time features, please use
+   [polling with ETag caching][polling-etag].
 1. Changes after submitting the merge request should be in separate commits
    (no squashing). If necessary, you will be asked to squash when the review is
    over, before merging.
@@ -400,6 +447,7 @@ the feature you contribute through all of these steps.
 1. Description explaining the relevancy (see following item)
 1. Working and clean code that is commented where needed
 1. Unit and integration tests that pass on the CI server
+1. Performance/scalability implications have been considered, addressed, and tested
 1. [Documented][doc-styleguide] in the /doc directory
 1. Changelog entry added
 1. Reviewed and any concerns are addressed
@@ -426,7 +474,7 @@ merge request:
 1.  [Ruby](https://github.com/bbatsov/ruby-style-guide).
     Important sections include [Source Code Layout][rss-source] and
     [Naming][rss-naming]. Use:
-    - multi-line method chaining style **Option B**: dot `.` on previous line
+    - multi-line method chaining style **Option A**: dot `.` on the second line
     - string literal quoting style **Option A**: single quoted by default
 1.  [Rails](https://github.com/bbatsov/rails-style-guide)
 1.  [Newlines styleguide][newlines-styleguide]
@@ -480,6 +528,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove
 available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
 
 [core team]: https://about.gitlab.com/core-team/
+[team]: https://about.gitlab.com/team/
 [getting-help]: https://about.gitlab.com/getting-help/
 [codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
 [accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
@@ -504,3 +553,9 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
 [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
 [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
 [license-finder-doc]: doc/development/licensing.md
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
+[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
+
+[^1]: Specs other than JavaScript specs are considered backend code. Haml
+      changes are considered backend code if they include Ruby code other than just
+      pure HTML.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
new file mode 100644
index 0000000000000000000000000000000000000000..0d91a54c7d439e84e3dd17d3594f1b2b6737f430
--- /dev/null
+++ b/GITALY_SERVER_VERSION
@@ -0,0 +1 @@
+0.3.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 9e11b32fcaa96816319e5d0dcff9fb2873f04061..1d0ba9ea182b0f7354f3daf12120744ec5e0c2f8 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.3.1
+0.4.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 627a3f43a64f9d878a1143f59bea46e1970d0320..0062ac971805f7b700058db4bb0f5c5b771dda76 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.1.1
+5.0.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index f0bb29e76388856b273698ae6064b0380ce5e5d2..347f5833ee6db7495cce808040501bf2c96269a9 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.3.0
+1.4.1
diff --git a/Gemfile b/Gemfile
index 01861f1ffac49b4e4c8158ae7b10484ce4216c2a..6af27ce0f3efd306bedbcb5bc8a10ef0c44b6bde 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres
 gem 'rugged', '~> 0.24.0'
 
 # Authentication libraries
-gem 'devise',                 '~> 4.2'
-gem 'doorkeeper',             '~> 4.2.0'
-gem 'omniauth',               '~> 1.3.2'
-gem 'omniauth-auth0',         '~> 1.4.1'
-gem 'omniauth-azure-oauth2',  '~> 0.0.6'
-gem 'omniauth-cas3',          '~> 1.1.2'
-gem 'omniauth-facebook',      '~> 4.0.0'
-gem 'omniauth-github',        '~> 1.1.1'
-gem 'omniauth-gitlab',        '~> 1.0.2'
+gem 'devise', '~> 4.2'
+gem 'doorkeeper', '~> 4.2.0'
+gem 'doorkeeper-openid_connect', '~> 1.1.0'
+gem 'omniauth', '~> 1.4.2'
+gem 'omniauth-auth0', '~> 1.4.1'
+gem 'omniauth-azure-oauth2', '~> 0.0.6'
+gem 'omniauth-cas3', '~> 1.1.2'
+gem 'omniauth-facebook', '~> 4.0.0'
+gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-gitlab', '~> 1.0.2'
 gem 'omniauth-google-oauth2', '~> 0.4.1'
-gem 'omniauth-kerberos',      '~> 0.3.0', group: :kerberos
+gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
 gem 'omniauth-oauth2-generic', '~> 0.2.2'
-gem 'omniauth-saml',          '~> 1.7.0'
-gem 'omniauth-shibboleth',    '~> 1.2.0'
-gem 'omniauth-twitter',       '~> 1.2.0'
-gem 'omniauth_crowd',         '~> 2.2.0'
-gem 'omniauth-authentiq',     '~> 0.3.0'
-gem 'rack-oauth2',            '~> 1.2.1'
-gem 'jwt',                    '~> 1.5.6'
+gem 'omniauth-saml', '~> 1.7.0'
+gem 'omniauth-shibboleth', '~> 1.2.0'
+gem 'omniauth-twitter', '~> 1.2.0'
+gem 'omniauth_crowd', '~> 2.2.0'
+gem 'omniauth-authentiq', '~> 0.3.0'
+gem 'rack-oauth2', '~> 1.2.1'
+gem 'jwt', '~> 1.5.6'
 
 # Spam and anti-bot protection
 gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
 gem 'github-linguist', '~> 4.7.0', require: 'linguist'
 
 # API
-gem 'grape',        '~> 0.18.0'
+gem 'grape', '~> 0.19.0'
 gem 'grape-entity', '~> 0.6.0'
-gem 'rack-cors',    '~> 0.4.0', require: 'rack/cors'
+gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
 
 # Pagination
 gem 'kaminari', '~> 0.17.0'
@@ -79,7 +80,7 @@ gem 'kaminari', '~> 0.17.0'
 gem 'hamlit', '~> 2.6.1'
 
 # Files attachments
-gem 'carrierwave', '~> 0.10.0'
+gem 'carrierwave', '~> 0.11.0'
 
 # Drag and Drop UI
 gem 'dropzonejs-rails', '~> 0.7.1'
@@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4'
 gem 'seed-fu', '~> 2.3.5'
 
 # Markdown and HTML processing
-gem 'html-pipeline',        '~> 1.11.0'
-gem 'deckar01-task_list',   '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup',        '~> 1.5.1'
-gem 'redcarpet',            '~> 3.3.3'
-gem 'RedCloth',             '~> 4.3.2'
-gem 'rdoc',                 '~> 4.2'
-gem 'org-ruby',             '~> 0.9.12'
-gem 'creole',               '~> 0.5.0'
-gem 'wikicloth',            '0.8.1'
-gem 'asciidoctor',          '~> 1.5.2'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.1'
+gem 'redcarpet', '~> 3.4'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~> 4.2'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
 gem 'asciidoctor-plantuml', '0.0.7'
-gem 'rouge',                '~> 2.0'
-gem 'truncato',             '~> 0.7.8'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
 
 # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
 # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -201,7 +202,7 @@ gem 'babosa', '~> 1.0.2'
 gem 'loofah', '~> 2.0.3'
 
 # Working with license
-gem 'licensee', '~> 8.0.0'
+gem 'licensee', '~> 8.7.0'
 
 # Protect against bruteforcing
 gem 'rack-attack', '~> 4.4.1'
@@ -229,19 +230,18 @@ gem 'sass-rails', '~> 5.0.6'
 gem 'coffee-rails', '~> 4.1.0'
 gem 'uglifier', '~> 2.7.2'
 
-gem 'addressable',        '~> 2.3.8'
-gem 'bootstrap-sass',     '~> 3.3.0'
-gem 'font-awesome-rails', '~> 4.6.1'
-gem 'gemojione',          '~> 3.0'
-gem 'gon',                '~> 6.1.0'
+gem 'addressable', '~> 2.3.8'
+gem 'bootstrap-sass', '~> 3.3.0'
+gem 'font-awesome-rails', '~> 4.7'
+gem 'gemojione', '~> 3.0'
+gem 'gon', '~> 6.1.0'
 gem 'jquery-atwho-rails', '~> 1.3.2'
-gem 'jquery-rails',       '~> 4.1.0'
-gem 'jquery-ui-rails',    '~> 5.0.0'
-gem 'request_store',      '~> 1.3'
-gem 'select2-rails',      '~> 3.5.9'
-gem 'virtus',             '~> 1.0.1'
-gem 'net-ssh',            '~> 3.0.1'
-gem 'base32',             '~> 0.3.0'
+gem 'jquery-rails', '~> 4.1.0'
+gem 'request_store', '~> 1.3'
+gem 'select2-rails', '~> 3.5.9'
+gem 'virtus', '~> 1.0.1'
+gem 'net-ssh', '~> 3.0.1'
+gem 'base32', '~> 0.3.0'
 
 # Sentry integration
 gem 'sentry-raven', '~> 2.0.0'
@@ -279,13 +279,13 @@ group :development, :test do
   gem 'awesome_print', '~> 1.2.0', require: false
   gem 'fuubar', '~> 2.0.0'
 
-  gem 'database_cleaner',   '~> 1.5.0'
+  gem 'database_cleaner', '~> 1.5.0'
   gem 'factory_girl_rails', '~> 4.7.0'
-  gem 'rspec-rails',        '~> 3.5.0'
-  gem 'rspec-retry',        '~> 0.4.5'
-  gem 'spinach-rails',      '~> 0.2.1'
+  gem 'rspec-rails', '~> 3.5.0'
+  gem 'rspec-retry', '~> 0.4.5'
+  gem 'spinach-rails', '~> 0.2.1'
   gem 'spinach-rerun-reporter', '~> 0.0.2'
-  gem 'rspec_profiling',    '~> 0.0.5'
+  gem 'rspec_profiling', '~> 0.0.5'
 
   # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
   gem 'minitest', '~> 5.7.0'
@@ -293,18 +293,18 @@ group :development, :test do
   # Generate Fake data
   gem 'ffaker', '~> 2.4'
 
-  gem 'capybara',            '~> 2.6.2'
+  gem 'capybara', '~> 2.6.2'
   gem 'capybara-screenshot', '~> 1.0.0'
-  gem 'poltergeist',         '~> 1.9.0'
+  gem 'poltergeist', '~> 1.9.0'
 
-  gem 'spring',                   '~> 1.7.0'
-  gem 'spring-commands-rspec',    '~> 1.0.4'
-  gem 'spring-commands-spinach',  '~> 1.1.0'
+  gem 'spring', '~> 1.7.0'
+  gem 'spring-commands-rspec', '~> 1.0.4'
+  gem 'spring-commands-spinach', '~> 1.1.0'
 
-  gem 'rubocop', '~> 0.46.0', require: false
-  gem 'rubocop-rspec', '~> 1.9.1', require: false
+  gem 'rubocop', '~> 0.47.1', require: false
+  gem 'rubocop-rspec', '~> 1.12.0', require: false
   gem 'scss_lint', '~> 0.47.0', require: false
-  gem 'haml_lint', '~> 0.18.2', require: false
+  gem 'haml_lint', '~> 0.21.0', require: false
   gem 'simplecov', '0.12.0', require: false
   gem 'flay', '~> 2.6.1', require: false
   gem 'bundler-audit', '~> 0.5.0', require: false
@@ -329,8 +329,6 @@ group :test do
   gem 'timecop', '~> 0.8.0'
 end
 
-gem 'newrelic_rpm', '~> 3.16'
-
 gem 'octokit', '~> 4.6.2'
 
 gem 'mail_room', '~> 0.9.1'
@@ -347,8 +345,11 @@ gem 'oauth2', '~> 1.2.0'
 gem 'paranoia', '~> 2.2'
 
 # Health check
-gem 'health_check', '~> 2.2.0'
+gem 'health_check', '~> 2.6.0'
 
 # System information
 gem 'vmstat', '~> 2.3.0'
 gem 'sys-filesystem', '~> 1.1.6'
+
+# Gitaly GRPC client
+gem 'gitaly', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2a3be7637531476a2cded4c27d1a0a087bc2b93d..043ca4f8800cf116ddd25fc31fc85949f7ed457e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,7 +2,7 @@ GEM
   remote: https://rubygems.org/
   specs:
     RedCloth (4.3.2)
-    ace-rails-ap (4.1.0)
+    ace-rails-ap (4.1.2)
     actionmailer (4.2.8)
       actionpack (= 4.2.8)
       actionview (= 4.2.8)
@@ -78,6 +78,7 @@ GEM
     better_errors (1.0.1)
       coderay (>= 1.0.0)
       erubis (>= 2.6.6)
+    bindata (2.3.5)
     binding_of_caller (0.7.2)
       debug_inspector (>= 0.0.1)
     bootstrap-sass (3.3.6)
@@ -103,11 +104,12 @@ GEM
     capybara-screenshot (1.0.11)
       capybara (>= 1.0, < 3)
       launchy
-    carrierwave (0.10.0)
+    carrierwave (0.11.2)
       activemodel (>= 3.2.0)
       activesupport (>= 3.2.0)
       json (>= 1.7)
       mime-types (>= 1.16)
+      mimemagic (>= 0.3.0)
     cause (0.1)
     charlock_holmes (0.7.3)
     chronic (0.10.2)
@@ -166,6 +168,9 @@ GEM
       unf (>= 0.0.5, < 1.0.0)
     doorkeeper (4.2.0)
       railties (>= 4.2)
+    doorkeeper-openid_connect (1.1.2)
+      doorkeeper (~> 4.0)
+      json-jwt (~> 1.6)
     dropzonejs-rails (0.7.2)
       rails (> 3.1)
     email_reply_trimmer (0.1.6)
@@ -231,7 +236,7 @@ GEM
     fog-xml (0.1.2)
       fog-core
       nokogiri (~> 1.5, >= 1.5.11)
-    font-awesome-rails (4.6.1.0)
+    font-awesome-rails (4.7.0.1)
       railties (>= 3.2, < 5.1)
     foreman (0.78.0)
       thor (~> 0.19.1)
@@ -245,6 +250,9 @@ GEM
       json
     get_process_mem (0.2.0)
     gherkin-ruby (0.3.2)
+    gitaly (0.3.0)
+      google-protobuf (~> 3.1)
+      grpc (~> 1.0)
     github-linguist (4.7.6)
       charlock_holmes (~> 0.7.3)
       escape_utils (~> 1.1.0)
@@ -296,6 +304,7 @@ GEM
       multi_json (~> 1.10)
       retriable (~> 1.4)
       signet (~> 0.6)
+    google-protobuf (3.2.0.2)
     googleauth (0.5.1)
       faraday (~> 0.9)
       jwt (~> 1.4)
@@ -304,7 +313,7 @@ GEM
       multi_json (~> 1.11)
       os (~> 0.9)
       signet (~> 0.7)
-    grape (0.18.0)
+    grape (0.19.1)
       activesupport
       builder
       hashie (>= 2.1.0)
@@ -317,19 +326,22 @@ GEM
     grape-entity (0.6.0)
       activesupport
       multi_json (>= 1.3.2)
+    grpc (1.1.2)
+      google-protobuf (~> 3.1)
+      googleauth (~> 0.5.1)
     haml (4.0.7)
       tilt
-    haml_lint (0.18.2)
+    haml_lint (0.21.0)
       haml (~> 4.0)
-      rake (>= 10, < 12)
-      rubocop (>= 0.36.0)
+      rake (>= 10, < 13)
+      rubocop (>= 0.47.0)
       sysexits (~> 1.1)
     hamlit (2.6.1)
       temple (~> 0.7.6)
       thor
       tilt
-    hashie (3.4.4)
-    health_check (2.2.1)
+    hashie (3.5.5)
+    health_check (2.6.0)
       rails (>= 4.0)
     hipchat (1.5.2)
       httparty
@@ -353,8 +365,8 @@ GEM
       json (~> 1.8)
       multi_xml (>= 0.5.2)
     httpclient (2.8.2)
-    i18n (0.8.0)
-    ice_nine (0.11.1)
+    i18n (0.8.1)
+    ice_nine (0.11.2)
     influxdb (0.2.3)
       cause
       json
@@ -367,9 +379,13 @@ GEM
       rails-dom-testing (>= 1, < 3)
       railties (>= 4.2.0)
       thor (>= 0.14, < 2.0)
-    jquery-ui-rails (5.0.5)
-      railties (>= 3.2.16)
     json (1.8.6)
+    json-jwt (1.7.1)
+      activesupport
+      bindata
+      multi_json (>= 1.3)
+      securecompare
+      url_safe_base64
     json-schema (2.6.2)
       addressable (~> 2.3.8)
     jwt (1.5.6)
@@ -398,8 +414,8 @@ GEM
       rubyzip
       thor
       xml-simple
-    licensee (8.0.0)
-      rugged (>= 0.24b)
+    licensee (8.7.0)
+      rugged (~> 0.24)
     little-plugger (1.1.4)
     logging (2.1.0)
       little-plugger (~> 1.1)
@@ -417,7 +433,7 @@ GEM
     minitest (5.7.0)
     mousetrap-rails (1.4.6)
     multi_json (1.12.1)
-    multi_xml (0.5.5)
+    multi_xml (0.6.0)
     multipart-post (2.0.0)
     mustermann (0.4.0)
       tool (~> 0.2)
@@ -427,7 +443,6 @@ GEM
     net-ldap (0.12.1)
     net-ssh (3.0.1)
     netrc (0.11.0)
-    newrelic_rpm (3.16.0.318)
     nokogiri (1.6.8.1)
       mini_portile2 (~> 2.1.0)
     numerizer (0.1.1)
@@ -441,7 +456,7 @@ GEM
     octokit (4.6.2)
       sawyer (~> 0.8.0, >= 0.5.3)
     oj (2.17.4)
-    omniauth (1.3.2)
+    omniauth (1.4.2)
       hashie (>= 1.2, < 4)
       rack (>= 1.0, < 3)
     omniauth-auth0 (1.4.1)
@@ -501,7 +516,7 @@ GEM
     os (0.9.6)
     paranoia (2.2.0)
       activerecord (>= 4.0, < 5.1)
-    parser (2.3.1.4)
+    parser (2.4.0.0)
       ast (~> 2.2)
     pg (0.18.4)
     poltergeist (1.9.0)
@@ -579,7 +594,7 @@ GEM
     recaptcha (3.0.0)
       json
     recursive-open-struct (1.0.0)
-    redcarpet (3.3.3)
+    redcarpet (3.4.0)
     redis (3.2.2)
     redis-actionpack (5.0.1)
       actionpack (>= 4.0, < 6)
@@ -642,13 +657,13 @@ GEM
       pg
       rails
       sqlite3
-    rubocop (0.46.0)
-      parser (>= 2.3.1.1, < 3.0)
+    rubocop (0.47.1)
+      parser (>= 2.3.3.1, < 3.0)
       powerpack (~> 0.1)
       rainbow (>= 1.99.1, < 3.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (~> 1.0, >= 1.0.1)
-    rubocop-rspec (1.9.1)
+    rubocop-rspec (1.12.0)
       rubocop (>= 0.42.0)
     ruby-fogbugz (0.2.1)
       crack (~> 0.4)
@@ -660,7 +675,7 @@ GEM
       sexp_processor (~> 4.1)
     rubyntlm (0.5.2)
     rubypants (0.2.0)
-    rubyzip (1.2.0)
+    rubyzip (1.2.1)
     rufus-scheduler (3.1.10)
     rugged (0.24.0)
     safe_yaml (1.0.4)
@@ -679,6 +694,7 @@ GEM
     scss_lint (0.47.1)
       rake (>= 0.9, < 11)
       sass (~> 3.4.15)
+    securecompare (1.0.0)
     seed-fu (2.3.6)
       activerecord (>= 3.1)
       activesupport (>= 3.1)
@@ -758,8 +774,8 @@ GEM
       eventmachine (~> 1.0, >= 1.0.4)
       rack (>= 1, < 3)
     thor (0.19.4)
-    thread_safe (0.3.5)
-    tilt (2.0.5)
+    thread_safe (0.3.6)
+    tilt (2.0.6)
     timecop (0.8.1)
     timfel-krb5-auth (0.8.3)
     tool (0.2.3)
@@ -776,7 +792,7 @@ GEM
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.2)
-    unicode-display_width (1.1.1)
+    unicode-display_width (1.1.3)
     unicorn (5.1.0)
       kgio (~> 2.6)
       raindrops (~> 0.7)
@@ -784,6 +800,7 @@ GEM
       get_process_mem (~> 0)
       unicorn (>= 4, < 6)
     uniform_notifier (1.10.0)
+    url_safe_base64 (0.2.2)
     validates_hostname (1.0.6)
       activerecord (>= 3.0)
       activesupport (>= 3.0)
@@ -846,7 +863,7 @@ DEPENDENCIES
   bundler-audit (~> 0.5.0)
   capybara (~> 2.6.2)
   capybara-screenshot (~> 1.0.0)
-  carrierwave (~> 0.10.0)
+  carrierwave (~> 0.11.0)
   charlock_holmes (~> 0.7.3)
   chronic (~> 0.10.2)
   chronic_duration (~> 0.10.6)
@@ -861,6 +878,7 @@ DEPENDENCIES
   devise-two-factor (~> 3.0.0)
   diffy (~> 3.1.0)
   doorkeeper (~> 4.2.0)
+  doorkeeper-openid_connect (~> 1.1.0)
   dropzonejs-rails (~> 0.7.1)
   email_reply_trimmer (~> 0.1)
   email_spec (~> 1.6.0)
@@ -873,11 +891,12 @@ DEPENDENCIES
   fog-local (~> 0.3)
   fog-openstack (~> 0.1)
   fog-rackspace (~> 0.1.1)
-  font-awesome-rails (~> 4.6.1)
+  font-awesome-rails (~> 4.7)
   foreman (~> 0.78.0)
   fuubar (~> 2.0.0)
   gemnasium-gitlab-service (~> 0.2)
   gemojione (~> 3.0)
+  gitaly (~> 0.3.0)
   github-linguist (~> 4.7.0)
   gitlab-flowdock-git-hook (~> 1.0.1)
   gitlab-markup (~> 1.5.1)
@@ -886,11 +905,11 @@ DEPENDENCIES
   gollum-rugged_adapter (~> 0.4.2)
   gon (~> 6.1.0)
   google-api-client (~> 0.8.6)
-  grape (~> 0.18.0)
+  grape (~> 0.19.0)
   grape-entity (~> 0.6.0)
-  haml_lint (~> 0.18.2)
+  haml_lint (~> 0.21.0)
   hamlit (~> 2.6.1)
-  health_check (~> 2.2.0)
+  health_check (~> 2.6.0)
   hipchat (~> 1.5.0)
   html-pipeline (~> 1.11.0)
   html2text
@@ -899,7 +918,6 @@ DEPENDENCIES
   jira-ruby (~> 1.1.2)
   jquery-atwho-rails (~> 1.3.2)
   jquery-rails (~> 4.1.0)
-  jquery-ui-rails (~> 5.0.0)
   json-schema (~> 2.6.2)
   jwt (~> 1.5.6)
   kaminari (~> 0.17.0)
@@ -907,7 +925,7 @@ DEPENDENCIES
   kubeclient (~> 2.2.0)
   letter_opener_web (~> 1.3.0)
   license_finder (~> 2.1.0)
-  licensee (~> 8.0.0)
+  licensee (~> 8.7.0)
   loofah (~> 2.0.3)
   mail_room (~> 0.9.1)
   method_source (~> 0.8)
@@ -915,12 +933,11 @@ DEPENDENCIES
   mousetrap-rails (~> 1.4.6)
   mysql2 (~> 0.3.16)
   net-ssh (~> 3.0.1)
-  newrelic_rpm (~> 3.16)
   nokogiri (~> 1.6.7, >= 1.6.7.2)
   oauth2 (~> 1.2.0)
   octokit (~> 4.6.2)
   oj (~> 2.17.4)
-  omniauth (~> 1.3.2)
+  omniauth (~> 1.4.2)
   omniauth-auth0 (~> 1.4.1)
   omniauth-authentiq (~> 0.3.0)
   omniauth-azure-oauth2 (~> 0.0.6)
@@ -952,7 +969,7 @@ DEPENDENCIES
   rblineprof (~> 0.3.6)
   rdoc (~> 4.2)
   recaptcha (~> 3.0)
-  redcarpet (~> 3.3.3)
+  redcarpet (~> 3.4)
   redis (~> 3.2)
   redis-namespace (~> 1.5.2)
   redis-rails (~> 5.0.1)
@@ -963,8 +980,8 @@ DEPENDENCIES
   rspec-rails (~> 3.5.0)
   rspec-retry (~> 0.4.5)
   rspec_profiling (~> 0.0.5)
-  rubocop (~> 0.46.0)
-  rubocop-rspec (~> 1.9.1)
+  rubocop (~> 0.47.1)
+  rubocop-rspec (~> 1.12.0)
   ruby-fogbugz (~> 0.2.1)
   ruby-prof (~> 0.16.2)
   rugged (~> 0.24.0)
@@ -1011,4 +1028,4 @@ DEPENDENCIES
   wikicloth (= 0.8.1)
 
 BUNDLED WITH
-   1.14.3
+   1.14.5
diff --git a/VERSION b/VERSION
index 5c99c061a476a2d06502fb459d72b82843af7851..64de83166748019693bd84007dd050c2e24b48c7 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.17.0-pre
+8.18.0-pre
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 6f1a34a559127963b82b87cf7bc00fa634545a78..5dcd9c09b70f8e6cefc41e9d1b8ce2773fc954b6 100644
Binary files a/app/assets/images/emoji.png and b/app/assets/images/emoji.png differ
diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png
new file mode 100644
index 0000000000000000000000000000000000000000..6903ff0304adbfe4e43af386e183dc60952b2f2e
Binary files /dev/null and b/app/assets/images/emoji/100.png differ
diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png
new file mode 100644
index 0000000000000000000000000000000000000000..248dc7e55b69a079df0e941e733a03e56cdf471d
Binary files /dev/null and b/app/assets/images/emoji/1234.png differ
diff --git a/app/assets/images/emoji/1F627.png b/app/assets/images/emoji/1F627.png
new file mode 100644
index 0000000000000000000000000000000000000000..f99026a3bc763644474f7b60a9c49ee220985b07
Binary files /dev/null and b/app/assets/images/emoji/1F627.png differ
diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png
new file mode 100644
index 0000000000000000000000000000000000000000..38ca662edede523d52c7460ad3d4fa27adf4488e
Binary files /dev/null and b/app/assets/images/emoji/8ball.png differ
diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png
new file mode 100644
index 0000000000000000000000000000000000000000..8603ff05a1798d346e757bcc5673148039e4c9b6
Binary files /dev/null and b/app/assets/images/emoji/a.png differ
diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png
new file mode 100644
index 0000000000000000000000000000000000000000..d9f2d17dea0b1c81e25aee5a62d22551df08eaaa
Binary files /dev/null and b/app/assets/images/emoji/ab.png differ
diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png
new file mode 100644
index 0000000000000000000000000000000000000000..7688de692a91d2178f575e8b27286a9048a9c59c
Binary files /dev/null and b/app/assets/images/emoji/abc.png differ
diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png
new file mode 100644
index 0000000000000000000000000000000000000000..0996a8705706585ac33f01a5d62f6f0f5bf68b79
Binary files /dev/null and b/app/assets/images/emoji/abcd.png differ
diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png
new file mode 100644
index 0000000000000000000000000000000000000000..8afd7ce99cfb05752058edb02ac8a090fd23e9ca
Binary files /dev/null and b/app/assets/images/emoji/accept.png differ
diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png
new file mode 100644
index 0000000000000000000000000000000000000000..3eb4b61bf1d3fe6557858a51e0b6a3743aed9c13
Binary files /dev/null and b/app/assets/images/emoji/aerial_tramway.png differ
diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png
new file mode 100644
index 0000000000000000000000000000000000000000..268d2ac3c8e062b73a1f2e20d9600daadf920650
Binary files /dev/null and b/app/assets/images/emoji/airplane.png differ
diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png
new file mode 100644
index 0000000000000000000000000000000000000000..d66841962f2c5f41cff1ad70c883691035749ede
Binary files /dev/null and b/app/assets/images/emoji/airplane_arriving.png differ
diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5766f9f4ae7b57a4c258a27d843f49e024ec564
Binary files /dev/null and b/app/assets/images/emoji/airplane_departure.png differ
diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png
new file mode 100644
index 0000000000000000000000000000000000000000..b731b15e3a827de32ffe430b582cd16d8133fe3f
Binary files /dev/null and b/app/assets/images/emoji/airplane_small.png differ
diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdbc2fbb950f69e5dad4ac3ea2f8023689bab1da
Binary files /dev/null and b/app/assets/images/emoji/alarm_clock.png differ
diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png
new file mode 100644
index 0000000000000000000000000000000000000000..307a73242496485805205689870eef694117438b
Binary files /dev/null and b/app/assets/images/emoji/alembic.png differ
diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b90e97433b9c042d6bc656dbd6080e971e97351
Binary files /dev/null and b/app/assets/images/emoji/alien.png differ
diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fb8076d7660627cb3c04e95f827c79351737509
Binary files /dev/null and b/app/assets/images/emoji/ambulance.png differ
diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png
new file mode 100644
index 0000000000000000000000000000000000000000..96de50560599be568b85cf731cbed03bc9078d53
Binary files /dev/null and b/app/assets/images/emoji/amphora.png differ
diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png
new file mode 100644
index 0000000000000000000000000000000000000000..b036f70a00b1527140863f361bea5a87d0c85d13
Binary files /dev/null and b/app/assets/images/emoji/anchor.png differ
diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png
new file mode 100644
index 0000000000000000000000000000000000000000..66ea97a3b9960a5e6550f5ce11c273a623ad8cd8
Binary files /dev/null and b/app/assets/images/emoji/angel.png differ
diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..391694dc07ef722fc25526f6fe4dba83118f9180
Binary files /dev/null and b/app/assets/images/emoji/angel_tone1.png differ
diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..700cbe6ed2c753f6fe661e76373d2a2e6ccdcffc
Binary files /dev/null and b/app/assets/images/emoji/angel_tone2.png differ
diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..be597437d2549706c7f999ce2f24b5194192acc0
Binary files /dev/null and b/app/assets/images/emoji/angel_tone3.png differ
diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b06d3c853efd4e688d257dd4ba8b3615ce0ecd95
Binary files /dev/null and b/app/assets/images/emoji/angel_tone4.png differ
diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..17bd677e33432af391a66776b72eb6bbb5364581
Binary files /dev/null and b/app/assets/images/emoji/angel_tone5.png differ
diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png
new file mode 100644
index 0000000000000000000000000000000000000000..d63c2e000e4f84f82e831f5b930bc6be2555bc98
Binary files /dev/null and b/app/assets/images/emoji/anger.png differ
diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5c97c4d29726eb9e2f8c0bf2084c58d64dfa7fc
Binary files /dev/null and b/app/assets/images/emoji/anger_right.png differ
diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png
new file mode 100644
index 0000000000000000000000000000000000000000..cfc4a6ecde529cc889a1a24a6532a10b0c804850
Binary files /dev/null and b/app/assets/images/emoji/angry.png differ
diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png
new file mode 100644
index 0000000000000000000000000000000000000000..994127ed6b3361a2c749ddb6f29b000e7a0e1f50
Binary files /dev/null and b/app/assets/images/emoji/ant.png differ
diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png
new file mode 100644
index 0000000000000000000000000000000000000000..da650c60f6256bac5a37a29251d29c69c3a5127c
Binary files /dev/null and b/app/assets/images/emoji/apple.png differ
diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png
new file mode 100644
index 0000000000000000000000000000000000000000..641a4f6888987f728b6e4b9b871f840aca7457d4
Binary files /dev/null and b/app/assets/images/emoji/aquarius.png differ
diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png
new file mode 100644
index 0000000000000000000000000000000000000000..21a189d0edee4f471e47376a9f7d89b7d22af903
Binary files /dev/null and b/app/assets/images/emoji/aries.png differ
diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee38e3b038e4475012c27997f06c2e5ced4f25d8
Binary files /dev/null and b/app/assets/images/emoji/arrow_backward.png differ
diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..90193bfcb4097a27ca59f0dafb6ba5030ff221f3
Binary files /dev/null and b/app/assets/images/emoji/arrow_double_down.png differ
diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..13543d5eef2375f5d7428aef8bfd48d53f14ce0d
Binary files /dev/null and b/app/assets/images/emoji/arrow_double_up.png differ
diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8eefd0b19f24fe450713a9288e95575d0fad8a8
Binary files /dev/null and b/app/assets/images/emoji/arrow_down.png differ
diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png
new file mode 100644
index 0000000000000000000000000000000000000000..5870b9a22418600928ca59e7f4cac2f6192bb6ac
Binary files /dev/null and b/app/assets/images/emoji/arrow_down_small.png differ
diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e2b682857cba2632bdc10f8853681f14326406c
Binary files /dev/null and b/app/assets/images/emoji/arrow_forward.png differ
diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..2d9d24bca800425df3b7d8fe27053b63bba171ac
Binary files /dev/null and b/app/assets/images/emoji/arrow_heading_down.png differ
diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..f29bfcfc0dec537f9d816a9f6796c2f3c2a13536
Binary files /dev/null and b/app/assets/images/emoji/arrow_heading_up.png differ
diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c685e0a81b0d55cf31c78d76f15ce43d0d8cc30
Binary files /dev/null and b/app/assets/images/emoji/arrow_left.png differ
diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..88b3771607845a4e906fb7fe6a95bc5ed811a72b
Binary files /dev/null and b/app/assets/images/emoji/arrow_lower_left.png differ
diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e807da739226c5d19e5b6c873df4f1e15d19b55
Binary files /dev/null and b/app/assets/images/emoji/arrow_lower_right.png differ
diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..4755670b5cc73f0f7928a4b58acd3c47e2461d0c
Binary files /dev/null and b/app/assets/images/emoji/arrow_right.png differ
diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png
new file mode 100644
index 0000000000000000000000000000000000000000..e7258ad32683eca24c1bcc7ee6d11d24bca08754
Binary files /dev/null and b/app/assets/images/emoji/arrow_right_hook.png differ
diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..af8218a87f7a810450a88d5047fe30e5bcac4f13
Binary files /dev/null and b/app/assets/images/emoji/arrow_up.png differ
diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..dfa32b9718672169da3d37fa1c92bc781b168f65
Binary files /dev/null and b/app/assets/images/emoji/arrow_up_down.png differ
diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png
new file mode 100644
index 0000000000000000000000000000000000000000..20a13dcd5cd47a073652362eca461cf82b0e0e33
Binary files /dev/null and b/app/assets/images/emoji/arrow_up_small.png differ
diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..f38718fbe34c91a83928b00c2f20b2ce1d84aad6
Binary files /dev/null and b/app/assets/images/emoji/arrow_upper_left.png differ
diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..c43e12d0f6429faf706cddaaeb21fe3068fc2f17
Binary files /dev/null and b/app/assets/images/emoji/arrow_upper_right.png differ
diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png
new file mode 100644
index 0000000000000000000000000000000000000000..26e49c38388db8b89a3c38ba0f4b8842598ce1e0
Binary files /dev/null and b/app/assets/images/emoji/arrows_clockwise.png differ
diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d06d8e091226bb64ad36f660f31868574925288
Binary files /dev/null and b/app/assets/images/emoji/arrows_counterclockwise.png differ
diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd6afe9ff06e8fe290cdbabe151d035815798afb
Binary files /dev/null and b/app/assets/images/emoji/art.png differ
diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png
new file mode 100644
index 0000000000000000000000000000000000000000..c821731713246ba41e7aabc1f5829580298aea9f
Binary files /dev/null and b/app/assets/images/emoji/articulated_lorry.png differ
diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f8e5113803c0e4163707239dbcae78348749afc
Binary files /dev/null and b/app/assets/images/emoji/asterisk.png differ
diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd0ac55ec8e40ce5d44e3a43f05d7eeb01fbe945
Binary files /dev/null and b/app/assets/images/emoji/astonished.png differ
diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png
new file mode 100644
index 0000000000000000000000000000000000000000..423fa07dd5d75a81590ca654a62f658d6736fbe2
Binary files /dev/null and b/app/assets/images/emoji/athletic_shoe.png differ
diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d935307b94c8e58b2837c925227c858967dd67a
Binary files /dev/null and b/app/assets/images/emoji/atm.png differ
diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f4567aa0938aff6d8ff45cd8522ffb6d9444189
Binary files /dev/null and b/app/assets/images/emoji/atom.png differ
diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png
new file mode 100644
index 0000000000000000000000000000000000000000..06f0d124aedf1890f9a31dda24f4a717d5c3b67a
Binary files /dev/null and b/app/assets/images/emoji/avocado.png differ
diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png
new file mode 100644
index 0000000000000000000000000000000000000000..25875bc6a14e3a83d0f0e675cc6b9bbb58f8e4ef
Binary files /dev/null and b/app/assets/images/emoji/b.png differ
diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4af92c63c70a01dee85395c090402fdf7f0d5b9
Binary files /dev/null and b/app/assets/images/emoji/baby.png differ
diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png
new file mode 100644
index 0000000000000000000000000000000000000000..2bd105241808b1f47599c9eec4f07106b790d967
Binary files /dev/null and b/app/assets/images/emoji/baby_bottle.png differ
diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png
new file mode 100644
index 0000000000000000000000000000000000000000..dccd96576eabe8e9526a84c34988b163cc666051
Binary files /dev/null and b/app/assets/images/emoji/baby_chick.png differ
diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png
new file mode 100644
index 0000000000000000000000000000000000000000..64a10b71710efed400e66fedeea6528564524a66
Binary files /dev/null and b/app/assets/images/emoji/baby_symbol.png differ
diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d20911d40dbd1ad4d6cb55279d51e5aa82d87ccc
Binary files /dev/null and b/app/assets/images/emoji/baby_tone1.png differ
diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0a9b30ed178905722f2aeeb8c84ee729f7eae76
Binary files /dev/null and b/app/assets/images/emoji/baby_tone2.png differ
diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..7de5286fac1550a594e0b70f537e3db5a4fbb16b
Binary files /dev/null and b/app/assets/images/emoji/baby_tone3.png differ
diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b7a86ac6156c718318620f8e01461bfe877f6af
Binary files /dev/null and b/app/assets/images/emoji/baby_tone4.png differ
diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe1be34cb884cd0e90987249087bd26ec3927254
Binary files /dev/null and b/app/assets/images/emoji/baby_tone5.png differ
diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png
new file mode 100644
index 0000000000000000000000000000000000000000..d32c5d4f17f8219d93ebd82d2bb4b0a808adff21
Binary files /dev/null and b/app/assets/images/emoji/back.png differ
diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png
new file mode 100644
index 0000000000000000000000000000000000000000..f38a485fbe4a85e7da1628ef8fc37d082a333c1f
Binary files /dev/null and b/app/assets/images/emoji/bacon.png differ
diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ba1570899097f380866492000e89bee8d16f0cd
Binary files /dev/null and b/app/assets/images/emoji/badminton.png differ
diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png
new file mode 100644
index 0000000000000000000000000000000000000000..409b593e78a1012be93bdb27562118031d4e3880
Binary files /dev/null and b/app/assets/images/emoji/baggage_claim.png differ
diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png
new file mode 100644
index 0000000000000000000000000000000000000000..07916fe6df155f983c5503884801a85fd020621c
Binary files /dev/null and b/app/assets/images/emoji/balloon.png differ
diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b6767aea9e426fd2c28a06bdc7092963ba057b0
Binary files /dev/null and b/app/assets/images/emoji/ballot_box.png differ
diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png
new file mode 100644
index 0000000000000000000000000000000000000000..284d957384780f8c34d96a41dd845fffa9ca5b4c
Binary files /dev/null and b/app/assets/images/emoji/ballot_box_with_check.png differ
diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d5e0e728a09dac2f0ca61eb2fc03f068e62d36b
Binary files /dev/null and b/app/assets/images/emoji/bamboo.png differ
diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4987279580c265f2951bee740c741d218f8e307
Binary files /dev/null and b/app/assets/images/emoji/banana.png differ
diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png
new file mode 100644
index 0000000000000000000000000000000000000000..58a9c528fca60965db236eccd44e82b1432ccdcc
Binary files /dev/null and b/app/assets/images/emoji/bangbang.png differ
diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png
new file mode 100644
index 0000000000000000000000000000000000000000..dffdcef36a1af2e0c04702ede13e9a79fb6606ed
Binary files /dev/null and b/app/assets/images/emoji/bank.png differ
diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png
new file mode 100644
index 0000000000000000000000000000000000000000..53c89455008d94b192728a0b60eca2457407af93
Binary files /dev/null and b/app/assets/images/emoji/bar_chart.png differ
diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png
new file mode 100644
index 0000000000000000000000000000000000000000..896f4d716cf191ade76a4c0b6b59dd2217ad616c
Binary files /dev/null and b/app/assets/images/emoji/barber.png differ
diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8463f1538b59241b18c4e4ca95de59e4a43096b
Binary files /dev/null and b/app/assets/images/emoji/baseball.png differ
diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png
new file mode 100644
index 0000000000000000000000000000000000000000..64c76b79c6dd069c8cc310aa199b270d7316bb1d
Binary files /dev/null and b/app/assets/images/emoji/basketball.png differ
diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ce90c5cad62250ff32b9db780b44ddb40857b94
Binary files /dev/null and b/app/assets/images/emoji/basketball_player.png differ
diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd12c7ab9bf487dc406a08bf409772114ad945f9
Binary files /dev/null and b/app/assets/images/emoji/basketball_player_tone1.png differ
diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f892fd596dafb2d6c76cda3e05d76c949a5b8e04
Binary files /dev/null and b/app/assets/images/emoji/basketball_player_tone2.png differ
diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..e109997a91a0099a83587dbfc93e042993540d76
Binary files /dev/null and b/app/assets/images/emoji/basketball_player_tone3.png differ
diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b90b946af4a46560993a1eedcd5497d1ddc40c6
Binary files /dev/null and b/app/assets/images/emoji/basketball_player_tone4.png differ
diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..bafed7828a7bc3668fde6c07bdb37a4748ba4c0c
Binary files /dev/null and b/app/assets/images/emoji/basketball_player_tone5.png differ
diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png
new file mode 100644
index 0000000000000000000000000000000000000000..3152c047e004e63281ff865056c67b97b101be51
Binary files /dev/null and b/app/assets/images/emoji/bat.png differ
diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png
new file mode 100644
index 0000000000000000000000000000000000000000..43fba5c8a28322f8afcf0a4cad4b08f2563a7786
Binary files /dev/null and b/app/assets/images/emoji/bath.png differ
diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2152eabf2f5eb2d0e50f382ccbed0404a529a4a7
Binary files /dev/null and b/app/assets/images/emoji/bath_tone1.png differ
diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..2102e6133e3d497092b37838b11962db25556dac
Binary files /dev/null and b/app/assets/images/emoji/bath_tone2.png differ
diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..fae66181e9f49317a792c01ce60b19050b34e84a
Binary files /dev/null and b/app/assets/images/emoji/bath_tone3.png differ
diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f8959d0d993ce7cbbb5452cfc4bad72647fb9e4
Binary files /dev/null and b/app/assets/images/emoji/bath_tone4.png differ
diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8a08e84f25f33c2380f63cf7935a608afc29271
Binary files /dev/null and b/app/assets/images/emoji/bath_tone5.png differ
diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a5f09361ebfbb22c15672b494e4dab35050c597
Binary files /dev/null and b/app/assets/images/emoji/bathtub.png differ
diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png
new file mode 100644
index 0000000000000000000000000000000000000000..f593e2bdb65b35898ea5502fb02e646a144d00c1
Binary files /dev/null and b/app/assets/images/emoji/battery.png differ
diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png
new file mode 100644
index 0000000000000000000000000000000000000000..69108c8ea109bb55e545c356453d40bd03f3db1f
Binary files /dev/null and b/app/assets/images/emoji/beach.png differ
diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png
new file mode 100644
index 0000000000000000000000000000000000000000..220a74f81325150a7371dcd3f357a6e92a5d48a0
Binary files /dev/null and b/app/assets/images/emoji/beach_umbrella.png differ
diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png
new file mode 100644
index 0000000000000000000000000000000000000000..272d56bbbcc70145adb0c0941a9628db2d76a37d
Binary files /dev/null and b/app/assets/images/emoji/bear.png differ
diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png
new file mode 100644
index 0000000000000000000000000000000000000000..86f964e245d106f5e8bcf3b309ea377e8dc42a64
Binary files /dev/null and b/app/assets/images/emoji/bed.png differ
diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png
new file mode 100644
index 0000000000000000000000000000000000000000..4615606009621bdd6b3f1e75a40b3c1e5c716710
Binary files /dev/null and b/app/assets/images/emoji/bee.png differ
diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6d73dc0b7a45e44cfea1025256d3004a9464db6
Binary files /dev/null and b/app/assets/images/emoji/beer.png differ
diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png
new file mode 100644
index 0000000000000000000000000000000000000000..b55deb66b4175b1416d21a253e785233c106553b
Binary files /dev/null and b/app/assets/images/emoji/beers.png differ
diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d93174d7fc9d16558024e1383f593d7898a417b
Binary files /dev/null and b/app/assets/images/emoji/beetle.png differ
diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc434fb7cb54d10143c9e84daf580b3d0fbd30d5
Binary files /dev/null and b/app/assets/images/emoji/beginner.png differ
diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b3b0461999acd8b60eb901005b1642a6375c9d2
Binary files /dev/null and b/app/assets/images/emoji/bell.png differ
diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b3297ceaf73d8af60bf16836d7bdb4034351119
Binary files /dev/null and b/app/assets/images/emoji/bellhop.png differ
diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png
new file mode 100644
index 0000000000000000000000000000000000000000..83d41ca7eb9bab3f56a0c2c3bf90b83901fbad2f
Binary files /dev/null and b/app/assets/images/emoji/bento.png differ
diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png
new file mode 100644
index 0000000000000000000000000000000000000000..9274da11048ea6542a0c2a6aebe07c050dbe7f2b
Binary files /dev/null and b/app/assets/images/emoji/bicyclist.png differ
diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..decc2f728fe339c6c25fe33014291aea069011ce
Binary files /dev/null and b/app/assets/images/emoji/bicyclist_tone1.png differ
diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0067717b80acfbb8f7aa69909c758f85c64536fe
Binary files /dev/null and b/app/assets/images/emoji/bicyclist_tone2.png differ
diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4f7b5e2776a288164d2f2b310e37aaa3716bf76
Binary files /dev/null and b/app/assets/images/emoji/bicyclist_tone3.png differ
diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3c8a797db428fc6f8368fa58fcb75afd84b54ac
Binary files /dev/null and b/app/assets/images/emoji/bicyclist_tone4.png differ
diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..1606a8740512cd0d4bd980e9947b78396e0e762f
Binary files /dev/null and b/app/assets/images/emoji/bicyclist_tone5.png differ
diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png
new file mode 100644
index 0000000000000000000000000000000000000000..556ed70f1a705fc9ac6cbd780d3e97965b6796fd
Binary files /dev/null and b/app/assets/images/emoji/bike.png differ
diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png
new file mode 100644
index 0000000000000000000000000000000000000000..77a8a0aae5b3d963ca59b184f68384cba74b1ee6
Binary files /dev/null and b/app/assets/images/emoji/bikini.png differ
diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png
new file mode 100644
index 0000000000000000000000000000000000000000..007b4fc2d85cca519ed555221d2df32f6fdd7d1a
Binary files /dev/null and b/app/assets/images/emoji/biohazard.png differ
diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png
new file mode 100644
index 0000000000000000000000000000000000000000..e201c22be335c811afaa88a5d332bfbfc104564a
Binary files /dev/null and b/app/assets/images/emoji/bird.png differ
diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png
new file mode 100644
index 0000000000000000000000000000000000000000..317e9a41949a2bf09bf5c196f579efa5a5249259
Binary files /dev/null and b/app/assets/images/emoji/birthday.png differ
diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png
new file mode 100644
index 0000000000000000000000000000000000000000..b62b87170e838fb0b0dbd94fc1f245c34f4f94f9
Binary files /dev/null and b/app/assets/images/emoji/black_circle.png differ
diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4068c3e6e80b381c2ebfe43b6d3c966a436e406
Binary files /dev/null and b/app/assets/images/emoji/black_heart.png differ
diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d0924b68aa326085d0d627164c6522e9b318575
Binary files /dev/null and b/app/assets/images/emoji/black_joker.png differ
diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..162f2bb429097122a82e31a78022a9ce036fb7c9
Binary files /dev/null and b/app/assets/images/emoji/black_large_square.png differ
diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..39765bba610195892095320fe4726fa2bd2f24c5
Binary files /dev/null and b/app/assets/images/emoji/black_medium_small_square.png differ
diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..05a30a6aa2d2080987fb371cc4dd66892cf2f466
Binary files /dev/null and b/app/assets/images/emoji/black_medium_square.png differ
diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png
new file mode 100644
index 0000000000000000000000000000000000000000..872d0ae1598817bda9ee2dae1f9db54deaffb5fd
Binary files /dev/null and b/app/assets/images/emoji/black_nib.png differ
diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..48595d3e1a92ac11104a7219b734c045d3443483
Binary files /dev/null and b/app/assets/images/emoji/black_small_square.png differ
diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..a78fc2f6b635e6b4203a3e59df8a6105692cb766
Binary files /dev/null and b/app/assets/images/emoji/black_square_button.png differ
diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png
new file mode 100644
index 0000000000000000000000000000000000000000..4083026c1574de527c2b1ec2bbb3df360a352f84
Binary files /dev/null and b/app/assets/images/emoji/blossom.png differ
diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png
new file mode 100644
index 0000000000000000000000000000000000000000..a10f4f84e357f9120e76ac60d6d79f7d6c7661d0
Binary files /dev/null and b/app/assets/images/emoji/blowfish.png differ
diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1e455401ccda0be5f71e02a5352d7b88f501837
Binary files /dev/null and b/app/assets/images/emoji/blue_book.png differ
diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8ba817d393f122838d794a2f6c4d329317ce3f4
Binary files /dev/null and b/app/assets/images/emoji/blue_car.png differ
diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..bdf1287e55e9166238983f23b22e66515a6cee9e
Binary files /dev/null and b/app/assets/images/emoji/blue_heart.png differ
diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png
new file mode 100644
index 0000000000000000000000000000000000000000..aac1a424ad4e8d344dbac828b85fc4d85b9d9714
Binary files /dev/null and b/app/assets/images/emoji/blush.png differ
diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png
new file mode 100644
index 0000000000000000000000000000000000000000..fead972633c2673bc53ec72bb39be5b22bede609
Binary files /dev/null and b/app/assets/images/emoji/boar.png differ
diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7f8f81c939f2ef9f749e6caa4c87559fa5ca406
Binary files /dev/null and b/app/assets/images/emoji/bomb.png differ
diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f4447ed3960e49333c2d3c88b049abf99de1c30
Binary files /dev/null and b/app/assets/images/emoji/book.png differ
diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png
new file mode 100644
index 0000000000000000000000000000000000000000..bbb444611f06d9d0ebd717262f0c3eca44d355c6
Binary files /dev/null and b/app/assets/images/emoji/bookmark.png differ
diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8d9e01b4286060f88533586e182080a87b322e8
Binary files /dev/null and b/app/assets/images/emoji/bookmark_tabs.png differ
diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png
new file mode 100644
index 0000000000000000000000000000000000000000..59a8bafeb0d1dc9b68efb3c64941d76ec064eb11
Binary files /dev/null and b/app/assets/images/emoji/books.png differ
diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b0f027b1a8e2c29293173ca2f5345d564b450cf
Binary files /dev/null and b/app/assets/images/emoji/boom.png differ
diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png
new file mode 100644
index 0000000000000000000000000000000000000000..11f1065ed07e7e77a395d8fb9334cc540708cb93
Binary files /dev/null and b/app/assets/images/emoji/boot.png differ
diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png
new file mode 100644
index 0000000000000000000000000000000000000000..11455af6df49d159cd001ac0579bd7457e924bfb
Binary files /dev/null and b/app/assets/images/emoji/bouquet.png differ
diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8f793088dcd1dfa2d352f3770917c35d58606a2
Binary files /dev/null and b/app/assets/images/emoji/bow.png differ
diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a538bf475f1d3dca03694956f3d93624cf2c55c
Binary files /dev/null and b/app/assets/images/emoji/bow_and_arrow.png differ
diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..87afb7b54cf636c5b481994f4ae8157d436801eb
Binary files /dev/null and b/app/assets/images/emoji/bow_tone1.png differ
diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ccf7dc085035f2517df5fab2ebe8507c1f52ec9
Binary files /dev/null and b/app/assets/images/emoji/bow_tone2.png differ
diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b9eb64f926b7ce5e4e1c4b41ff0391710fd0eda
Binary files /dev/null and b/app/assets/images/emoji/bow_tone3.png differ
diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..683795ff40d7aee2b44351ac2e6e17977a8990fd
Binary files /dev/null and b/app/assets/images/emoji/bow_tone4.png differ
diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..7969d9717521094016042f387a38437956c25d77
Binary files /dev/null and b/app/assets/images/emoji/bow_tone5.png differ
diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png
new file mode 100644
index 0000000000000000000000000000000000000000..63add89e53b33e3beb0cf3a5e21f33892c9271a6
Binary files /dev/null and b/app/assets/images/emoji/bowling.png differ
diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png
new file mode 100644
index 0000000000000000000000000000000000000000..9838f24e51a750788b18b35cf825ce7b428a2db1
Binary files /dev/null and b/app/assets/images/emoji/boxing_glove.png differ
diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ecfb0a4e92a16b757c568ccf1fee362515da0b5
Binary files /dev/null and b/app/assets/images/emoji/boy.png differ
diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fc436ea512bf699f47a137ded06012ac2896b02
Binary files /dev/null and b/app/assets/images/emoji/boy_tone1.png differ
diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..09a5f18d360933b7c4ab8a0cd6c9f26df46e36e7
Binary files /dev/null and b/app/assets/images/emoji/boy_tone2.png differ
diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cfe675dd3a6a7fb5ca2ff55d251ebaa32475e88
Binary files /dev/null and b/app/assets/images/emoji/boy_tone3.png differ
diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..780be0ace3661b205315d3d1549b104447dad37c
Binary files /dev/null and b/app/assets/images/emoji/boy_tone4.png differ
diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..f32fe22e35c248598d306c3010f0dc0e79bf6f53
Binary files /dev/null and b/app/assets/images/emoji/boy_tone5.png differ
diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png
new file mode 100644
index 0000000000000000000000000000000000000000..6676510aaa572fe29a27ae4f14a0646907a30dbc
Binary files /dev/null and b/app/assets/images/emoji/bread.png differ
diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png
new file mode 100644
index 0000000000000000000000000000000000000000..eaf4bd97890ac49f2c0918be86242e5a60c7ed60
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil.png differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4fb141ae8f5b32d8d2775721cd75c2999f13f04
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil_tone1.png differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c248769fc06454ebdd774e3cb3b9107c786ab9ef
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil_tone2.png differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..962c0a6eedb77aa9bcaae150f4c38f1cc49106c3
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil_tone3.png differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..740ca208cd45cde0de0365e4d02e0f5646604b15
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil_tone4.png differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..5cc5598587dcb9edf92fd560f7e2a70ab9bfa40a
Binary files /dev/null and b/app/assets/images/emoji/bride_with_veil_tone5.png differ
diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png
new file mode 100644
index 0000000000000000000000000000000000000000..1d444e0be65b91a346e75253cec6a48e39101075
Binary files /dev/null and b/app/assets/images/emoji/bridge_at_night.png differ
diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9912ba2148ebb7c60774f4776f7ebf4b404e086
Binary files /dev/null and b/app/assets/images/emoji/briefcase.png differ
diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..718e26ee12230f2fc824bfd8d3507a726555b336
Binary files /dev/null and b/app/assets/images/emoji/broken_heart.png differ
diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png
new file mode 100644
index 0000000000000000000000000000000000000000..e64e72f259a3df9eb0837311736c6c40d9189817
Binary files /dev/null and b/app/assets/images/emoji/bug.png differ
diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png
new file mode 100644
index 0000000000000000000000000000000000000000..38e32e02d9f5c7a81a699af0536c320d023f2d51
Binary files /dev/null and b/app/assets/images/emoji/bulb.png differ
diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f698e056fa1f924a300d4e1d56feee570b8dea0
Binary files /dev/null and b/app/assets/images/emoji/bullettrain_front.png differ
diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed61c67bf075ee08f2f0dd691e6475711209a239
Binary files /dev/null and b/app/assets/images/emoji/bullettrain_side.png differ
diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png
new file mode 100644
index 0000000000000000000000000000000000000000..02bd5601df7e0dc123fb26f384b310d6b532b0a2
Binary files /dev/null and b/app/assets/images/emoji/burrito.png differ
diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png
new file mode 100644
index 0000000000000000000000000000000000000000..641ddc56ca70095bb71bd7cce61ed8cf57a4fa77
Binary files /dev/null and b/app/assets/images/emoji/bus.png differ
diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2b62208bfd640fd6369cecab41e54b11e4a987f
Binary files /dev/null and b/app/assets/images/emoji/busstop.png differ
diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png
new file mode 100644
index 0000000000000000000000000000000000000000..123b2cbe1fbf799c3cd4bc3cccaf75578057fb58
Binary files /dev/null and b/app/assets/images/emoji/bust_in_silhouette.png differ
diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7656860a1c0f09e6861f7813aceb547ecf6d501
Binary files /dev/null and b/app/assets/images/emoji/busts_in_silhouette.png differ
diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png
new file mode 100644
index 0000000000000000000000000000000000000000..5631fe992265c60f0067c3033aa93cdb45152e5c
Binary files /dev/null and b/app/assets/images/emoji/butterfly.png differ
diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b48ccf3d0c5301310bc126453d9d7c6edf303ee
Binary files /dev/null and b/app/assets/images/emoji/cactus.png differ
diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png
new file mode 100644
index 0000000000000000000000000000000000000000..4368177be9acf8f56d54ced3d78437aac22da787
Binary files /dev/null and b/app/assets/images/emoji/cake.png differ
diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png
new file mode 100644
index 0000000000000000000000000000000000000000..47353b744471cb303322a2cc2d6c7c0f97fdd324
Binary files /dev/null and b/app/assets/images/emoji/calendar.png differ
diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png
new file mode 100644
index 0000000000000000000000000000000000000000..dec8d49bfa8c3f5e2c3231d2b8956c9d0db195c1
Binary files /dev/null and b/app/assets/images/emoji/calendar_spiral.png differ
diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png
new file mode 100644
index 0000000000000000000000000000000000000000..a10c59ba7114367c4fe0891538a8247dbc5640c9
Binary files /dev/null and b/app/assets/images/emoji/call_me.png differ
diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c93201181a57524a464f4fb28b74313e8fc9ab5
Binary files /dev/null and b/app/assets/images/emoji/call_me_tone1.png differ
diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c39f45a41ed659ff301990d0ee7de1d34e46dc6f
Binary files /dev/null and b/app/assets/images/emoji/call_me_tone2.png differ
diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..83a57f63c291ff704cd0d5d5b5d778d857057d07
Binary files /dev/null and b/app/assets/images/emoji/call_me_tone3.png differ
diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..65b3468fe44d59c066d39284304797b825aa9143
Binary files /dev/null and b/app/assets/images/emoji/call_me_tone4.png differ
diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..94ef68ff3b3b004887278b792af1afdf5506b7c7
Binary files /dev/null and b/app/assets/images/emoji/call_me_tone5.png differ
diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2f308f8e461bf4549f7eb1cb9a3b8fe15bfbcea
Binary files /dev/null and b/app/assets/images/emoji/calling.png differ
diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png
new file mode 100644
index 0000000000000000000000000000000000000000..b421d07a8055c27ffde4c0ac411e0179ff6871a6
Binary files /dev/null and b/app/assets/images/emoji/camel.png differ
diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a3429f72eff31140c6de282f54abfa51f2d8faf
Binary files /dev/null and b/app/assets/images/emoji/camera.png differ
diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png
new file mode 100644
index 0000000000000000000000000000000000000000..27471da2029d865e1a2bec60589f0b6f12b1dbcb
Binary files /dev/null and b/app/assets/images/emoji/camera_with_flash.png differ
diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png
new file mode 100644
index 0000000000000000000000000000000000000000..d589cc1f44bc756ea8e79880749abeea4adfabf5
Binary files /dev/null and b/app/assets/images/emoji/camping.png differ
diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png
new file mode 100644
index 0000000000000000000000000000000000000000..a64af07cb5fef796bda726af269887bfe1481edf
Binary files /dev/null and b/app/assets/images/emoji/cancer.png differ
diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b56444e35535c8630904b64b3d4702a935bc72a
Binary files /dev/null and b/app/assets/images/emoji/candle.png differ
diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c67ace3a35ccbf6b8703749814ef4c8cc22cc42
Binary files /dev/null and b/app/assets/images/emoji/candy.png differ
diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png
new file mode 100644
index 0000000000000000000000000000000000000000..e26cdb9da69836a10d522ffd0f6813bb560990a7
Binary files /dev/null and b/app/assets/images/emoji/canoe.png differ
diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe9482d2d8a79dea8c78bd840d376abc56a614fb
Binary files /dev/null and b/app/assets/images/emoji/capital_abcd.png differ
diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png
new file mode 100644
index 0000000000000000000000000000000000000000..6293d31d4b161afea77f3dd6309c7c0e9db9a70f
Binary files /dev/null and b/app/assets/images/emoji/capricorn.png differ
diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2e764ce59de8924b1562e5199b63a0fec8ada28
Binary files /dev/null and b/app/assets/images/emoji/card_box.png differ
diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png
new file mode 100644
index 0000000000000000000000000000000000000000..151e11cb3b46c00d18dec25ae879c3d3039c22e3
Binary files /dev/null and b/app/assets/images/emoji/card_index.png differ
diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png
new file mode 100644
index 0000000000000000000000000000000000000000..a17074edf05e156d642fb8edf3b881c6433870e0
Binary files /dev/null and b/app/assets/images/emoji/carousel_horse.png differ
diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png
new file mode 100644
index 0000000000000000000000000000000000000000..c68829b58e78835e849ec5cc7ca8089183656c85
Binary files /dev/null and b/app/assets/images/emoji/carrot.png differ
diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png
new file mode 100644
index 0000000000000000000000000000000000000000..cbcaa578253a919c1bf83d8bfba1783d3b09586d
Binary files /dev/null and b/app/assets/images/emoji/cartwheel.png differ
diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..db6d65895fb1a920a144ce5de62e66b175469f88
Binary files /dev/null and b/app/assets/images/emoji/cartwheel_tone1.png differ
diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..e00ffbc27a81f475c51683c40df8e3633a17b2a2
Binary files /dev/null and b/app/assets/images/emoji/cartwheel_tone2.png differ
diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..49321be391f21d80735b8b1cfc869d16fe4ed5dc
Binary files /dev/null and b/app/assets/images/emoji/cartwheel_tone3.png differ
diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4562b5e3dddec643735ef688ce2fb4844dc5e4c
Binary files /dev/null and b/app/assets/images/emoji/cartwheel_tone4.png differ
diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e09a8707675193c50c43da93e36ed0dfd272126
Binary files /dev/null and b/app/assets/images/emoji/cartwheel_tone5.png differ
diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..efd82c2abf374075a3e9f5f39d320f2af05cb568
Binary files /dev/null and b/app/assets/images/emoji/cat.png differ
diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png
new file mode 100644
index 0000000000000000000000000000000000000000..46abe8cbc149c83df1e904c9c690035bf0bec7bf
Binary files /dev/null and b/app/assets/images/emoji/cat2.png differ
diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png
new file mode 100644
index 0000000000000000000000000000000000000000..e6b01449cd9a279689d31bc730a22ecb75f4a9c3
Binary files /dev/null and b/app/assets/images/emoji/cd.png differ
diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png
new file mode 100644
index 0000000000000000000000000000000000000000..57f46139a06df0d3fd324b3090c46c96a90b5b81
Binary files /dev/null and b/app/assets/images/emoji/chains.png differ
diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png
new file mode 100644
index 0000000000000000000000000000000000000000..285a79a93d0010483d48b4adf5c92d573f112966
Binary files /dev/null and b/app/assets/images/emoji/champagne.png differ
diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png
new file mode 100644
index 0000000000000000000000000000000000000000..31937ae939244768074f16e7a4c10681bc8018a1
Binary files /dev/null and b/app/assets/images/emoji/champagne_glass.png differ
diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png
new file mode 100644
index 0000000000000000000000000000000000000000..9773f03be22822f133fa2b327453933767ab3859
Binary files /dev/null and b/app/assets/images/emoji/chart.png differ
diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png
new file mode 100644
index 0000000000000000000000000000000000000000..5222ec72d8540d9eb91bf83f867d809b5e31d725
Binary files /dev/null and b/app/assets/images/emoji/chart_with_downwards_trend.png differ
diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png
new file mode 100644
index 0000000000000000000000000000000000000000..f13cfcf99568913ec9a5795a509de52b3e97bf32
Binary files /dev/null and b/app/assets/images/emoji/chart_with_upwards_trend.png differ
diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a71eecb89be43d2c1e6cd3f99ff52ee333f6eac
Binary files /dev/null and b/app/assets/images/emoji/checkered_flag.png differ
diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png
new file mode 100644
index 0000000000000000000000000000000000000000..00e997622868cf943e6c444b760b934a539a33b7
Binary files /dev/null and b/app/assets/images/emoji/cheese.png differ
diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b10cbaac5e64dae5a1578c00e940f7cf7ef61c3
Binary files /dev/null and b/app/assets/images/emoji/cherries.png differ
diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png
new file mode 100644
index 0000000000000000000000000000000000000000..282f3e7bc8170321fda67060671ff73d6004b384
Binary files /dev/null and b/app/assets/images/emoji/cherry_blossom.png differ
diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9fb40468eda8f9953349e31d99d622c9d5b188b
Binary files /dev/null and b/app/assets/images/emoji/chestnut.png differ
diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a6992e55ba955ae3fe0fd6bda12f30a69702445
Binary files /dev/null and b/app/assets/images/emoji/chicken.png differ
diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa4c091c7c35fc2311fb8405b6bf4bd1a9227891
Binary files /dev/null and b/app/assets/images/emoji/children_crossing.png differ
diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png
new file mode 100644
index 0000000000000000000000000000000000000000..2aac560cb2230b727a8d4c910e47e337cb6caad7
Binary files /dev/null and b/app/assets/images/emoji/chipmunk.png differ
diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png
new file mode 100644
index 0000000000000000000000000000000000000000..318bbd40ef9c3c0fb3a900402bdd7c7c0bbdabe5
Binary files /dev/null and b/app/assets/images/emoji/chocolate_bar.png differ
diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..4197d37a52bca661c382073417172b5f68de7885
Binary files /dev/null and b/app/assets/images/emoji/christmas_tree.png differ
diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png
new file mode 100644
index 0000000000000000000000000000000000000000..8242fd272b3947c2d50a18c7f9ac91d305036ea0
Binary files /dev/null and b/app/assets/images/emoji/church.png differ
diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png
new file mode 100644
index 0000000000000000000000000000000000000000..65f27b386f248e5fe3caf4b9eb4fea6e8d6e7024
Binary files /dev/null and b/app/assets/images/emoji/cinema.png differ
diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0379775b12680b873b971a542dbcf876ead1505
Binary files /dev/null and b/app/assets/images/emoji/circus_tent.png differ
diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png
new file mode 100644
index 0000000000000000000000000000000000000000..80cdff7cf5dbd818bad4852e2108a0d0eacb5846
Binary files /dev/null and b/app/assets/images/emoji/city_dusk.png differ
diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cded0ba55b1c31a7d80d9ae6623005cf8fae138
Binary files /dev/null and b/app/assets/images/emoji/city_sunset.png differ
diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7b9844a0b4810a0426a3388345b5615a2ebebad
Binary files /dev/null and b/app/assets/images/emoji/cityscape.png differ
diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b01b4343e2435760f8c60d56db085d2eb4255bc
Binary files /dev/null and b/app/assets/images/emoji/cl.png differ
diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0ffe9289205ea061876135e7c61613218447aa2
Binary files /dev/null and b/app/assets/images/emoji/clap.png differ
diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..de4bc837b9681b2127ea93c9a65407d057eb08da
Binary files /dev/null and b/app/assets/images/emoji/clap_tone1.png differ
diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..1323de775bab6280683743147adc004ab014a662
Binary files /dev/null and b/app/assets/images/emoji/clap_tone2.png differ
diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d448ca19ddeead541410e504ba747832993fbf75
Binary files /dev/null and b/app/assets/images/emoji/clap_tone3.png differ
diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..c49f44ee91d97e341a527a151dc2f0ebf3a7fe5a
Binary files /dev/null and b/app/assets/images/emoji/clap_tone4.png differ
diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..29ee9bdf37c3aa5ec186fb35372e1e91b7853d2a
Binary files /dev/null and b/app/assets/images/emoji/clap_tone5.png differ
diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png
new file mode 100644
index 0000000000000000000000000000000000000000..81390883111ae087140925ee74bea64b876d3ade
Binary files /dev/null and b/app/assets/images/emoji/clapper.png differ
diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png
new file mode 100644
index 0000000000000000000000000000000000000000..de7b559daaf444c9aa512096a693a6bb6ef1cc59
Binary files /dev/null and b/app/assets/images/emoji/classical_building.png differ
diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..7edcfc52509b2900fe8a24e99f488582f40b6fb0
Binary files /dev/null and b/app/assets/images/emoji/clipboard.png differ
diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffdb451e3a8cf7b76ec74c812fbefb126148e2a8
Binary files /dev/null and b/app/assets/images/emoji/clock.png differ
diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6e34941f232fe8bcd33be06fc4d07c4c45d0d62
Binary files /dev/null and b/app/assets/images/emoji/clock1.png differ
diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png
new file mode 100644
index 0000000000000000000000000000000000000000..e62b245cdbe97a652f2212f98132daddcb04b147
Binary files /dev/null and b/app/assets/images/emoji/clock10.png differ
diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png
new file mode 100644
index 0000000000000000000000000000000000000000..0802b3c65b931fa72ba160522c7b8d3aeb0d8234
Binary files /dev/null and b/app/assets/images/emoji/clock1030.png differ
diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png
new file mode 100644
index 0000000000000000000000000000000000000000..0983345273b6c9e717bf659ca1ad329b5eff6ce8
Binary files /dev/null and b/app/assets/images/emoji/clock11.png differ
diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png
new file mode 100644
index 0000000000000000000000000000000000000000..d970d03b8095cbe03fb458ec9f500fc71449e01b
Binary files /dev/null and b/app/assets/images/emoji/clock1130.png differ
diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png
new file mode 100644
index 0000000000000000000000000000000000000000..e61caa4b3e2965d63761e97dc991eb5c798f9b72
Binary files /dev/null and b/app/assets/images/emoji/clock12.png differ
diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2b1d261721f223ee4645c68fd2856b8328d2db9
Binary files /dev/null and b/app/assets/images/emoji/clock1230.png differ
diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png
new file mode 100644
index 0000000000000000000000000000000000000000..86b7689b84e127aa57d35ec3a7e751c69d0dd555
Binary files /dev/null and b/app/assets/images/emoji/clock130.png differ
diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png
new file mode 100644
index 0000000000000000000000000000000000000000..a54253d7d570edd418df022f67c6bef9ab4b4794
Binary files /dev/null and b/app/assets/images/emoji/clock2.png differ
diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a787e018e618cd6135699a60fcb760d02e48d4f
Binary files /dev/null and b/app/assets/images/emoji/clock230.png differ
diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png
new file mode 100644
index 0000000000000000000000000000000000000000..27ec4b1f514c3229faa7384a5edc17cfdecd3833
Binary files /dev/null and b/app/assets/images/emoji/clock3.png differ
diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6860395cec441f17352824cbd0d18b8a91bfd6b
Binary files /dev/null and b/app/assets/images/emoji/clock330.png differ
diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png
new file mode 100644
index 0000000000000000000000000000000000000000..60a1ef4cc1300d2dcf3a0047724ae3ea32a28735
Binary files /dev/null and b/app/assets/images/emoji/clock4.png differ
diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c05b362122fb264043fdc0ffa37b55e7470ed13
Binary files /dev/null and b/app/assets/images/emoji/clock430.png differ
diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9382d1e0941c5bcef9f804a67cc4035aee5ca69
Binary files /dev/null and b/app/assets/images/emoji/clock5.png differ
diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png
new file mode 100644
index 0000000000000000000000000000000000000000..c21fa926db238f5af3b590feab2aa74e8d8b8a32
Binary files /dev/null and b/app/assets/images/emoji/clock530.png differ
diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fd5d3f5bd7669f05126160ecfb5f10b45c4d1cd
Binary files /dev/null and b/app/assets/images/emoji/clock6.png differ
diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png
new file mode 100644
index 0000000000000000000000000000000000000000..2aec87fefcfc50edd27fbee9620fa8c8ceab4115
Binary files /dev/null and b/app/assets/images/emoji/clock630.png differ
diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c7084036f22fcb3991318d12a0f0c9cd7f9eff6
Binary files /dev/null and b/app/assets/images/emoji/clock7.png differ
diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7a1135e03fe59f539b0c7daeb105590d187d672
Binary files /dev/null and b/app/assets/images/emoji/clock730.png differ
diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png
new file mode 100644
index 0000000000000000000000000000000000000000..fcddf722e95bd763089292d0faebfef879630935
Binary files /dev/null and b/app/assets/images/emoji/clock8.png differ
diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png
new file mode 100644
index 0000000000000000000000000000000000000000..799b4aebc08bf0c2988e2b1987b4de195b219859
Binary files /dev/null and b/app/assets/images/emoji/clock830.png differ
diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png
new file mode 100644
index 0000000000000000000000000000000000000000..dfbe011798131d32d3cc6d7e5e716403af9f949e
Binary files /dev/null and b/app/assets/images/emoji/clock9.png differ
diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a2092ee6f05b25e90ccd5ecd1c7a681814b4507
Binary files /dev/null and b/app/assets/images/emoji/clock930.png differ
diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png
new file mode 100644
index 0000000000000000000000000000000000000000..6395cf2151e55f1049ee11ce585eb0d540fbd76c
Binary files /dev/null and b/app/assets/images/emoji/closed_book.png differ
diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c1cd5d07417516feaf927ce8fd3d2c10c6dd874
Binary files /dev/null and b/app/assets/images/emoji/closed_lock_with_key.png differ
diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png
new file mode 100644
index 0000000000000000000000000000000000000000..ecefba9e446ae0d84f2d142a6b6f52117deb8bb9
Binary files /dev/null and b/app/assets/images/emoji/closed_umbrella.png differ
diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b4f57f77ba6c9b62600a93ff11d5b35d35ef55e
Binary files /dev/null and b/app/assets/images/emoji/cloud.png differ
diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png
new file mode 100644
index 0000000000000000000000000000000000000000..0831e88aa31687b4842e53d31806586f758c2c1d
Binary files /dev/null and b/app/assets/images/emoji/cloud_lightning.png differ
diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png
new file mode 100644
index 0000000000000000000000000000000000000000..385685e0512e56a17c337df6a4b4048a8270ce06
Binary files /dev/null and b/app/assets/images/emoji/cloud_rain.png differ
diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png
new file mode 100644
index 0000000000000000000000000000000000000000..9720384eb99eae55fde8e47e4f3772b8a761b55a
Binary files /dev/null and b/app/assets/images/emoji/cloud_snow.png differ
diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png
new file mode 100644
index 0000000000000000000000000000000000000000..4821c89da1e0f6fedcb04e56570bd579a2c012f7
Binary files /dev/null and b/app/assets/images/emoji/cloud_tornado.png differ
diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png
new file mode 100644
index 0000000000000000000000000000000000000000..02b7ff7004991407e957133a3d3a98a61b2c283e
Binary files /dev/null and b/app/assets/images/emoji/clown.png differ
diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f2abf791cacfe95eac6643edb4cd2ab7af717c4
Binary files /dev/null and b/app/assets/images/emoji/clubs.png differ
diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e50c57e98d54c5224433c96b066a27354465068
Binary files /dev/null and b/app/assets/images/emoji/cocktail.png differ
diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png
new file mode 100644
index 0000000000000000000000000000000000000000..553061471b14ca931efa3aae3b529cbab4cf18a1
Binary files /dev/null and b/app/assets/images/emoji/coffee.png differ
diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb2932aa5f6d7f030d23b5f3cbff0ba616870437
Binary files /dev/null and b/app/assets/images/emoji/coffin.png differ
diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png
new file mode 100644
index 0000000000000000000000000000000000000000..85b2231bbf6405b303c8cc0e9199f93aba34a67d
Binary files /dev/null and b/app/assets/images/emoji/cold_sweat.png differ
diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png
new file mode 100644
index 0000000000000000000000000000000000000000..a99751f79befcb581d05365652f716848fb40191
Binary files /dev/null and b/app/assets/images/emoji/comet.png differ
diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7eda7f362a04823a50c4228e6fa73c899feca9f
Binary files /dev/null and b/app/assets/images/emoji/compression.png differ
diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1fee27e3a9f580670f9771b2c0e537d242f63ed
Binary files /dev/null and b/app/assets/images/emoji/computer.png differ
diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba4fd9b12be2332bfda9c9d4d7ac367be5fc46a4
Binary files /dev/null and b/app/assets/images/emoji/confetti_ball.png differ
diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa4b29e9375e2980efe08e0fc4c4821b0291ae83
Binary files /dev/null and b/app/assets/images/emoji/confounded.png differ
diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png
new file mode 100644
index 0000000000000000000000000000000000000000..502b6bf0e0b4248e66dbd72d63a194986e425f6b
Binary files /dev/null and b/app/assets/images/emoji/confused.png differ
diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba8c89d95eeede6cabc9a11b4070f397c24c2f50
Binary files /dev/null and b/app/assets/images/emoji/congratulations.png differ
diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef8db5f471c2986a0726e2cedea620d26a7d75c1
Binary files /dev/null and b/app/assets/images/emoji/construction.png differ
diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png
new file mode 100644
index 0000000000000000000000000000000000000000..8206a20f63fb6f887309b476a5653637816f7871
Binary files /dev/null and b/app/assets/images/emoji/construction_site.png differ
diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9970a89005192b88092cc2caa4ad90565120537
Binary files /dev/null and b/app/assets/images/emoji/construction_worker.png differ
diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f24a2bab2445cd5afcef8332fd38a5db7d96228
Binary files /dev/null and b/app/assets/images/emoji/construction_worker_tone1.png differ
diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..93c8fec5a750ec46ac3b37a2f12b9d21c6b2e900
Binary files /dev/null and b/app/assets/images/emoji/construction_worker_tone2.png differ
diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..abc1f2af2e0a6856456422a3a98d9134b7df9d09
Binary files /dev/null and b/app/assets/images/emoji/construction_worker_tone3.png differ
diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..eed83289aeb5c95865f60db2188b9f5476c57e7d
Binary files /dev/null and b/app/assets/images/emoji/construction_worker_tone4.png differ
diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..acbb220b8bb04d6c4506b417e0e6f67956158d69
Binary files /dev/null and b/app/assets/images/emoji/construction_worker_tone5.png differ
diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png
new file mode 100644
index 0000000000000000000000000000000000000000..6635ac93b500d27377ccf4ba9faf39bae246f306
Binary files /dev/null and b/app/assets/images/emoji/control_knobs.png differ
diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png
new file mode 100644
index 0000000000000000000000000000000000000000..26b53b5669e172284f7d74291151f9c56ebab462
Binary files /dev/null and b/app/assets/images/emoji/convenience_store.png differ
diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b6bcb1554f7740097530ed40a65a59fd50be566
Binary files /dev/null and b/app/assets/images/emoji/cookie.png differ
diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png
new file mode 100644
index 0000000000000000000000000000000000000000..918c980577a8dc9ee7d3e69abbf81a962c0e5f6e
Binary files /dev/null and b/app/assets/images/emoji/cooking.png differ
diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png
new file mode 100644
index 0000000000000000000000000000000000000000..74674978d007c9ab08127a7b30c8021b398971b9
Binary files /dev/null and b/app/assets/images/emoji/cool.png differ
diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b16d7c17b7bfaf9ba470945a95d502d287b80ab
Binary files /dev/null and b/app/assets/images/emoji/cop.png differ
diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ccba3879dcb93adddd7e9c179f542df74800575
Binary files /dev/null and b/app/assets/images/emoji/cop_tone1.png differ
diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..7814ea9f52dec1c9155ee4e0a66ba899d432beff
Binary files /dev/null and b/app/assets/images/emoji/cop_tone2.png differ
diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d78e88ec8728c3cb8063ba92b2c111c0b9e0a345
Binary files /dev/null and b/app/assets/images/emoji/cop_tone3.png differ
diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e13c508315749f7bfa569678031378ab0a9e44c
Binary files /dev/null and b/app/assets/images/emoji/cop_tone4.png differ
diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..2980d61cc2e23c335ed33083da7194160b5fdc78
Binary files /dev/null and b/app/assets/images/emoji/cop_tone5.png differ
diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b9a6adbfd29d1973aaee59ec68317cffbb7ab03
Binary files /dev/null and b/app/assets/images/emoji/copyright.png differ
diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png
new file mode 100644
index 0000000000000000000000000000000000000000..36e20127931ba64ff6c5809164f035e61631dc9b
Binary files /dev/null and b/app/assets/images/emoji/corn.png differ
diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png
new file mode 100644
index 0000000000000000000000000000000000000000..27b19b13bb097148a91ce23db418d7794a12f269
Binary files /dev/null and b/app/assets/images/emoji/couch.png differ
diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png
new file mode 100644
index 0000000000000000000000000000000000000000..960323f3c16109494cdc6f64506172e7a8ac663e
Binary files /dev/null and b/app/assets/images/emoji/couple.png differ
diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png
new file mode 100644
index 0000000000000000000000000000000000000000..8759fa5db87da62360bdb7e31663749d78bd064e
Binary files /dev/null and b/app/assets/images/emoji/couple_mm.png differ
diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..62111601b36fa5fe9d1e1b7e8a0c3260acea9d60
Binary files /dev/null and b/app/assets/images/emoji/couple_with_heart.png differ
diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png
new file mode 100644
index 0000000000000000000000000000000000000000..08fdabcdc5c0651684f8a6d62606fbfd67db0d79
Binary files /dev/null and b/app/assets/images/emoji/couple_ww.png differ
diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png
new file mode 100644
index 0000000000000000000000000000000000000000..9aa519da9e83ad8e641a0d1c95eb3bf363aa23db
Binary files /dev/null and b/app/assets/images/emoji/couplekiss.png differ
diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png
new file mode 100644
index 0000000000000000000000000000000000000000..718a3986d647afc2bcc2749154262c6317ade4a6
Binary files /dev/null and b/app/assets/images/emoji/cow.png differ
diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d0ca534ff189b21fb8b332187f43df0cff5191e
Binary files /dev/null and b/app/assets/images/emoji/cow2.png differ
diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png
new file mode 100644
index 0000000000000000000000000000000000000000..70dd5d0d9d1f329f0f82e1d61de291a167a1a7d5
Binary files /dev/null and b/app/assets/images/emoji/cowboy.png differ
diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png
new file mode 100644
index 0000000000000000000000000000000000000000..19f3047ab61f48d2805e30dd8e7df0cab4b1817b
Binary files /dev/null and b/app/assets/images/emoji/crab.png differ
diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d7b427aaa3646daf536e9a709366d122a46abab
Binary files /dev/null and b/app/assets/images/emoji/crayon.png differ
diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png
new file mode 100644
index 0000000000000000000000000000000000000000..372777d5c6193c4ebe3be1a8d9e5ec5cbf324703
Binary files /dev/null and b/app/assets/images/emoji/credit_card.png differ
diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..765420ecec77738d22e3c2fc880f770ee3da7eca
Binary files /dev/null and b/app/assets/images/emoji/crescent_moon.png differ
diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png
new file mode 100644
index 0000000000000000000000000000000000000000..d602294a2cd1e0c5319a6ca54397546536f9eaf2
Binary files /dev/null and b/app/assets/images/emoji/cricket.png differ
diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png
new file mode 100644
index 0000000000000000000000000000000000000000..3005c46f1764e34e00108e5f8749990937f8650f
Binary files /dev/null and b/app/assets/images/emoji/crocodile.png differ
diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb33feb1a383fe21e10b0ef8853dce2d931c896e
Binary files /dev/null and b/app/assets/images/emoji/croissant.png differ
diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png
new file mode 100644
index 0000000000000000000000000000000000000000..42b10e82257069ef0ef6eb4d47c84cdf019717a0
Binary files /dev/null and b/app/assets/images/emoji/cross.png differ
diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png
new file mode 100644
index 0000000000000000000000000000000000000000..273bd0f0fe518224d51ebb49ea4dee9d986ed23d
Binary files /dev/null and b/app/assets/images/emoji/crossed_flags.png differ
diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png
new file mode 100644
index 0000000000000000000000000000000000000000..907e96071344a82a92b32555d01944df5eff728f
Binary files /dev/null and b/app/assets/images/emoji/crossed_swords.png differ
diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png
new file mode 100644
index 0000000000000000000000000000000000000000..93b82d92f0446171c3510287b50085ea066f160f
Binary files /dev/null and b/app/assets/images/emoji/crown.png differ
diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png
new file mode 100644
index 0000000000000000000000000000000000000000..19d4acbe40c829956533ad5cb68f836355375e5d
Binary files /dev/null and b/app/assets/images/emoji/cruise_ship.png differ
diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png
new file mode 100644
index 0000000000000000000000000000000000000000..b7877f8a1737914542c4bc621b03079577888c97
Binary files /dev/null and b/app/assets/images/emoji/cry.png differ
diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4f49715e00169bc1dc1d2ea810bdddcafcbcf06
Binary files /dev/null and b/app/assets/images/emoji/crying_cat_face.png differ
diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png
new file mode 100644
index 0000000000000000000000000000000000000000..485d5c888f1dffac26caf4569c539310df0cd200
Binary files /dev/null and b/app/assets/images/emoji/crystal_ball.png differ
diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png
new file mode 100644
index 0000000000000000000000000000000000000000..500807059d24e2df08f0f4c48e8b1dfb2a5cda4c
Binary files /dev/null and b/app/assets/images/emoji/cucumber.png differ
diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png
new file mode 100644
index 0000000000000000000000000000000000000000..2df0078ddd157319b703685806a30210ab41f2df
Binary files /dev/null and b/app/assets/images/emoji/cupid.png differ
diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png
new file mode 100644
index 0000000000000000000000000000000000000000..440aa56d50e0aa6fae0a6d73e6803da4e97e9975
Binary files /dev/null and b/app/assets/images/emoji/curly_loop.png differ
diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d46c6050e7da893096bb686c60a05b54f5d2fbd
Binary files /dev/null and b/app/assets/images/emoji/currency_exchange.png differ
diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png
new file mode 100644
index 0000000000000000000000000000000000000000..69657ca810336e24d757f9685a634f664f8fe302
Binary files /dev/null and b/app/assets/images/emoji/curry.png differ
diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa3df67b8f63f392a08eeb4fb7d04c258fb7a46b
Binary files /dev/null and b/app/assets/images/emoji/custard.png differ
diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png
new file mode 100644
index 0000000000000000000000000000000000000000..21b7ce2c69e1def64f7e28b0ecc1451ae50839b5
Binary files /dev/null and b/app/assets/images/emoji/customs.png differ
diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff00b1afe706cf0b392289c9f05b361bfdef6fa8
Binary files /dev/null and b/app/assets/images/emoji/cyclone.png differ
diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png
new file mode 100644
index 0000000000000000000000000000000000000000..66e97b0aa25a211fc8b6d40079c50916dec35cff
Binary files /dev/null and b/app/assets/images/emoji/dagger.png differ
diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png
new file mode 100644
index 0000000000000000000000000000000000000000..04b166991cb101802fac23a5376f85e3004c8946
Binary files /dev/null and b/app/assets/images/emoji/dancer.png differ
diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c7b11c3a6eaca18c369269f8ccdb1174ec174f4
Binary files /dev/null and b/app/assets/images/emoji/dancer_tone1.png differ
diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb04b1f907e4c8c92db1e0be83de3d4e9cdd70ef
Binary files /dev/null and b/app/assets/images/emoji/dancer_tone2.png differ
diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..98c5bca7b648c4c53da261973681aaddb3a1e196
Binary files /dev/null and b/app/assets/images/emoji/dancer_tone3.png differ
diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdb1e00cbba76744f1e03780a665bee86e1d70a3
Binary files /dev/null and b/app/assets/images/emoji/dancer_tone4.png differ
diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e34e0e23f057e4fbd07a55618ca509d6f9cf518
Binary files /dev/null and b/app/assets/images/emoji/dancer_tone5.png differ
diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png
new file mode 100644
index 0000000000000000000000000000000000000000..67e6ffacb76e71b31aa956cde3b616835889b7e4
Binary files /dev/null and b/app/assets/images/emoji/dancers.png differ
diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png
new file mode 100644
index 0000000000000000000000000000000000000000..f73f37b01c739a3e3a2f1877365483132ed5c362
Binary files /dev/null and b/app/assets/images/emoji/dango.png differ
diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1b6db0acff178ce9f318ee6ccc2ebb22823d5db
Binary files /dev/null and b/app/assets/images/emoji/dark_sunglasses.png differ
diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6704aeb8ba2708f0b526e184dc4b9067029c630
Binary files /dev/null and b/app/assets/images/emoji/dart.png differ
diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png
new file mode 100644
index 0000000000000000000000000000000000000000..064b8525c12fc3733ff0bc4d180e84bdfb91bd7a
Binary files /dev/null and b/app/assets/images/emoji/dash.png differ
diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png
new file mode 100644
index 0000000000000000000000000000000000000000..f05b3da97b8a073912944ff88e17b742ed81437d
Binary files /dev/null and b/app/assets/images/emoji/date.png differ
diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..785fc1c30eadfd63ca5642247b2ec9b22d31a437
Binary files /dev/null and b/app/assets/images/emoji/deciduous_tree.png differ
diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8698195ff0adf9f5fe6cb08b5ba7acde2749d84
Binary files /dev/null and b/app/assets/images/emoji/deer.png differ
diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png
new file mode 100644
index 0000000000000000000000000000000000000000..58867c7a6e151c8fa16329b8ffbf4ab75c2c9295
Binary files /dev/null and b/app/assets/images/emoji/department_store.png differ
diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9966ff8c65e4443e481a725a84294b14c63cca7
Binary files /dev/null and b/app/assets/images/emoji/desert.png differ
diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png
new file mode 100644
index 0000000000000000000000000000000000000000..909bd42b5e14034122d8399447f1a11709cad2c0
Binary files /dev/null and b/app/assets/images/emoji/desktop.png differ
diff --git a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a22a26d1e2a3e3925d5c448b8462083f1ec546a
Binary files /dev/null and b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png differ
diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f25f51f97ae9d4b0d8db51056c6f49d378517a6
Binary files /dev/null and b/app/assets/images/emoji/diamonds.png differ
diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png
new file mode 100644
index 0000000000000000000000000000000000000000..efe4e67e23cc6690e2af681643741146b8184af4
Binary files /dev/null and b/app/assets/images/emoji/disappointed.png differ
diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png
new file mode 100644
index 0000000000000000000000000000000000000000..aef864d2b3d7ab23f5b1ffb089934564ff8ecd1d
Binary files /dev/null and b/app/assets/images/emoji/disappointed_relieved.png differ
diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png
new file mode 100644
index 0000000000000000000000000000000000000000..46a7e403f9d564651974a3d318c8d8799b29729a
Binary files /dev/null and b/app/assets/images/emoji/dividers.png differ
diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png
new file mode 100644
index 0000000000000000000000000000000000000000..85f52efad2494c220ec5e78f6bcb596ea12d4b9d
Binary files /dev/null and b/app/assets/images/emoji/dizzy.png differ
diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..3120316ab5ebb2814f18b1aa2b42f081c9129a42
Binary files /dev/null and b/app/assets/images/emoji/dizzy_face.png differ
diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png
new file mode 100644
index 0000000000000000000000000000000000000000..341d2575f4f484939cf04454dd08b9ae0ffefe72
Binary files /dev/null and b/app/assets/images/emoji/do_not_litter.png differ
diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png
new file mode 100644
index 0000000000000000000000000000000000000000..281b81d58bd2fe2cb212bcbb46b5172fa25b5ebe
Binary files /dev/null and b/app/assets/images/emoji/dog.png differ
diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png
new file mode 100644
index 0000000000000000000000000000000000000000..976143dbdbebac0f8204d871368c4892c2734fdb
Binary files /dev/null and b/app/assets/images/emoji/dog2.png differ
diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9904c2829377bb9f20bcf332382f72deb36246e
Binary files /dev/null and b/app/assets/images/emoji/dollar.png differ
diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png
new file mode 100644
index 0000000000000000000000000000000000000000..1095561511043622953edaf1d354d2ed85a979dd
Binary files /dev/null and b/app/assets/images/emoji/dolls.png differ
diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png
new file mode 100644
index 0000000000000000000000000000000000000000..814348090031fca5c558bd0f3d6713320c822cde
Binary files /dev/null and b/app/assets/images/emoji/dolphin.png differ
diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png
new file mode 100644
index 0000000000000000000000000000000000000000..36ae3e274946f5c29347c6f841aaa9182e508e7a
Binary files /dev/null and b/app/assets/images/emoji/door.png differ
diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ca4cd0bde8f5b80cd4bee9fcb58b83616bfd23b
Binary files /dev/null and b/app/assets/images/emoji/doughnut.png differ
diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png
new file mode 100644
index 0000000000000000000000000000000000000000..9580c4917d70b74dd00e8cac1854441843c03229
Binary files /dev/null and b/app/assets/images/emoji/dove.png differ
diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6311cf54297aada1a40f1bba0db9b201c9d9f42
Binary files /dev/null and b/app/assets/images/emoji/dragon.png differ
diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c2720446c6f5524431771e3d3bbdc0f17c20651
Binary files /dev/null and b/app/assets/images/emoji/dragon_face.png differ
diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png
new file mode 100644
index 0000000000000000000000000000000000000000..a697ca5c57d5c8e78d81cdae198d29c08949ce22
Binary files /dev/null and b/app/assets/images/emoji/dress.png differ
diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png
new file mode 100644
index 0000000000000000000000000000000000000000..5271637c7c40094e9193d5304d4c433761af62de
Binary files /dev/null and b/app/assets/images/emoji/dromedary_camel.png differ
diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5460532597ea9b02a8b6d463aeb93319c97a69e
Binary files /dev/null and b/app/assets/images/emoji/drooling_face.png differ
diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png
new file mode 100644
index 0000000000000000000000000000000000000000..71241ec3061bd9e5b2f238b7954465f3250734b4
Binary files /dev/null and b/app/assets/images/emoji/droplet.png differ
diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png
new file mode 100644
index 0000000000000000000000000000000000000000..b038727cc99f9202d0e7d6d76f582d05ef774b51
Binary files /dev/null and b/app/assets/images/emoji/drum.png differ
diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png
new file mode 100644
index 0000000000000000000000000000000000000000..74330b77ca33bcb3824d55300fb9ac946c83f958
Binary files /dev/null and b/app/assets/images/emoji/duck.png differ
diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png
new file mode 100644
index 0000000000000000000000000000000000000000..045a6f7a08d3e6326771969b8659b547317c6cd8
Binary files /dev/null and b/app/assets/images/emoji/dvd.png differ
diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png
new file mode 100644
index 0000000000000000000000000000000000000000..d22e654a20bc76a14697625234cac84252102dfb
Binary files /dev/null and b/app/assets/images/emoji/e-mail.png differ
diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f277debeef3e8f268073b198d6c0aa12889aa3f
Binary files /dev/null and b/app/assets/images/emoji/eagle.png differ
diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png
new file mode 100644
index 0000000000000000000000000000000000000000..f84f9ff154aa219605f99fa6dd4697563d60b4b8
Binary files /dev/null and b/app/assets/images/emoji/ear.png differ
diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png
new file mode 100644
index 0000000000000000000000000000000000000000..3564d9d643a89da3d5762b38ed5358e0208f24d9
Binary files /dev/null and b/app/assets/images/emoji/ear_of_rice.png differ
diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d09e1e4199628fdd1d1e0e5357360079b2acf0c9
Binary files /dev/null and b/app/assets/images/emoji/ear_tone1.png differ
diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..300d60a9948faabc2a8ab1d153b4d358625bf2b0
Binary files /dev/null and b/app/assets/images/emoji/ear_tone2.png differ
diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a56eebe445fe8b3e8e0b3c7bf74b9603299cb83
Binary files /dev/null and b/app/assets/images/emoji/ear_tone3.png differ
diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd270f7763e83d13f535e627d786a22e2a53e333
Binary files /dev/null and b/app/assets/images/emoji/ear_tone4.png differ
diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..b96bb441dff23f386809d83dcdca26a125dfce84
Binary files /dev/null and b/app/assets/images/emoji/ear_tone5.png differ
diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png
new file mode 100644
index 0000000000000000000000000000000000000000..66c3348c23a07ed0a0cd8a186e07942ec89c8ccf
Binary files /dev/null and b/app/assets/images/emoji/earth_africa.png differ
diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png
new file mode 100644
index 0000000000000000000000000000000000000000..538c3cddd68eab40fd82dc303abfd3a71fd1af5c
Binary files /dev/null and b/app/assets/images/emoji/earth_americas.png differ
diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8df97fec3c42d4ec18b8cf638028aac133901dc
Binary files /dev/null and b/app/assets/images/emoji/earth_asia.png differ
diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png
new file mode 100644
index 0000000000000000000000000000000000000000..c171974d99350bced4c9096d71c130515ecd1e4e
Binary files /dev/null and b/app/assets/images/emoji/egg.png differ
diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png
new file mode 100644
index 0000000000000000000000000000000000000000..fafd7c1a14c5b7c761a898fd44cc6c606f6afbc4
Binary files /dev/null and b/app/assets/images/emoji/eggplant.png differ
diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c95874d4c526c61294a7aa2244784c5d8d1d0ae
Binary files /dev/null and b/app/assets/images/emoji/eight.png differ
diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png
new file mode 100644
index 0000000000000000000000000000000000000000..820179bda507c61db31bce2c096a4f1fc684ae99
Binary files /dev/null and b/app/assets/images/emoji/eight_pointed_black_star.png differ
diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png
new file mode 100644
index 0000000000000000000000000000000000000000..3307ffa62eee639f3eff931304008453f8cd4a54
Binary files /dev/null and b/app/assets/images/emoji/eight_spoked_asterisk.png differ
diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec5cfc489733d704ebdaf9e7ef4efc3afa6df98a
Binary files /dev/null and b/app/assets/images/emoji/eject.png differ
diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png
new file mode 100644
index 0000000000000000000000000000000000000000..31d1eb215b4818f3ad42d8b5939d91a9a5d9f26c
Binary files /dev/null and b/app/assets/images/emoji/electric_plug.png differ
diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8a6d1405958bfa06d8f0974451deb4df111dafd
Binary files /dev/null and b/app/assets/images/emoji/elephant.png differ
diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef3ccd5f367b763c50770599abf979d2f5c0dc0c
Binary files /dev/null and b/app/assets/images/emoji/end.png differ
diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec77ac375a40c22f746e29eec8a4cc3f84b41e41
Binary files /dev/null and b/app/assets/images/emoji/envelope.png differ
diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..7448a6b76735ccfbcf8adeafacb9d423c536bcdd
Binary files /dev/null and b/app/assets/images/emoji/envelope_with_arrow.png differ
diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png
new file mode 100644
index 0000000000000000000000000000000000000000..a49020820e1d76d4d1f03a1abc279d0821602f73
Binary files /dev/null and b/app/assets/images/emoji/euro.png differ
diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png
new file mode 100644
index 0000000000000000000000000000000000000000..888d11332ce14ced1122e6f3ccf25f5979688683
Binary files /dev/null and b/app/assets/images/emoji/european_castle.png differ
diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png
new file mode 100644
index 0000000000000000000000000000000000000000..3745aff8dd2162fe1ecddf2f2ab6c52a9e82e1b1
Binary files /dev/null and b/app/assets/images/emoji/european_post_office.png differ
diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..f679d8dd772049c5490d83bfc007b44c4e15a5d2
Binary files /dev/null and b/app/assets/images/emoji/evergreen_tree.png differ
diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c14406422f030bf2f3dd8a54ccf08596d191b35
Binary files /dev/null and b/app/assets/images/emoji/exclamation.png differ
diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png
new file mode 100644
index 0000000000000000000000000000000000000000..2954017f6c2a7e6c3c32509fe18e24c058ffeef1
Binary files /dev/null and b/app/assets/images/emoji/expressionless.png differ
diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d989cdd3753ffb6127b3199ebf64d59dc7ae2d9
Binary files /dev/null and b/app/assets/images/emoji/eye.png differ
diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png
new file mode 100644
index 0000000000000000000000000000000000000000..21bd22bbcced30976589b779a77c15ae1556cc48
Binary files /dev/null and b/app/assets/images/emoji/eye_in_speech_bubble.png differ
diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png
new file mode 100644
index 0000000000000000000000000000000000000000..865d8274acf9c39aab265947c1992511bc4c713c
Binary files /dev/null and b/app/assets/images/emoji/eyeglasses.png differ
diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..2102ada7e09b442e52f3a1502f41dc665fc28ef0
Binary files /dev/null and b/app/assets/images/emoji/eyes.png differ
diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png
new file mode 100644
index 0000000000000000000000000000000000000000..defc796cf161a8a80d6b69074a3616c161051766
Binary files /dev/null and b/app/assets/images/emoji/face_palm.png differ
diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f4b010bb40edbf4a7a6b880f172d11846347670
Binary files /dev/null and b/app/assets/images/emoji/face_palm_tone1.png differ
diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..97fb6831687a34a34c95689a3bfe4b78854f93bf
Binary files /dev/null and b/app/assets/images/emoji/face_palm_tone2.png differ
diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5b5c1e5306c5c3173c31b9705a983e435b12ee8
Binary files /dev/null and b/app/assets/images/emoji/face_palm_tone3.png differ
diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2840b11348343b343fd907af4652256d5463d692
Binary files /dev/null and b/app/assets/images/emoji/face_palm_tone4.png differ
diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f070db98be250ba4246834c5931b881a4706f00
Binary files /dev/null and b/app/assets/images/emoji/face_palm_tone5.png differ
diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1d2ddf4a2789409d8fc3687ac6209c4bd3313ac
Binary files /dev/null and b/app/assets/images/emoji/factory.png differ
diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d60e7bdf2d895589192cc120c95b696c385b399
Binary files /dev/null and b/app/assets/images/emoji/fallen_leaf.png differ
diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png
new file mode 100644
index 0000000000000000000000000000000000000000..2642196579189edade36a25be6a6716fc261ebf9
Binary files /dev/null and b/app/assets/images/emoji/family.png differ
diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a2e4e2c4910074f5794d07511b781e0eb5466da
Binary files /dev/null and b/app/assets/images/emoji/family_mmb.png differ
diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png
new file mode 100644
index 0000000000000000000000000000000000000000..81e6c0fc0ee24e0b30159cdb81d66d98f85d56cb
Binary files /dev/null and b/app/assets/images/emoji/family_mmbb.png differ
diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png
new file mode 100644
index 0000000000000000000000000000000000000000..932a85e1fe5d17ebf7fe2b4ae51cd1ce62b97a04
Binary files /dev/null and b/app/assets/images/emoji/family_mmg.png differ
diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png
new file mode 100644
index 0000000000000000000000000000000000000000..41e35166670b140753a4848e632fe83b05277161
Binary files /dev/null and b/app/assets/images/emoji/family_mmgb.png differ
diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e8ccfe6c7f0a58762374b21f373c3e5d1515eae
Binary files /dev/null and b/app/assets/images/emoji/family_mmgg.png differ
diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png
new file mode 100644
index 0000000000000000000000000000000000000000..b544fbe573fa23efe07dff7aea6583a671e5191d
Binary files /dev/null and b/app/assets/images/emoji/family_mwbb.png differ
diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png
new file mode 100644
index 0000000000000000000000000000000000000000..71d2681c32a20bdde830b36cef282ff36f9badbc
Binary files /dev/null and b/app/assets/images/emoji/family_mwg.png differ
diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png
new file mode 100644
index 0000000000000000000000000000000000000000..40dbf1f7a18e6700a6e81211d819f1f82b3adb29
Binary files /dev/null and b/app/assets/images/emoji/family_mwgb.png differ
diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png
new file mode 100644
index 0000000000000000000000000000000000000000..bfefa4879cb24aace15f43fb6e1c7eda8085c348
Binary files /dev/null and b/app/assets/images/emoji/family_mwgg.png differ
diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png
new file mode 100644
index 0000000000000000000000000000000000000000..836feae7c7865d22f88d4858e41ccb31d4ef4134
Binary files /dev/null and b/app/assets/images/emoji/family_wwb.png differ
diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c6ba45e7bb68574c627f2470d85f918b4412e82
Binary files /dev/null and b/app/assets/images/emoji/family_wwbb.png differ
diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png
new file mode 100644
index 0000000000000000000000000000000000000000..41225c6fa5a0b17c096d0879efd9430733732823
Binary files /dev/null and b/app/assets/images/emoji/family_wwg.png differ
diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png
new file mode 100644
index 0000000000000000000000000000000000000000..284d29ab5da60d95f4e6852d1ed105e123e41880
Binary files /dev/null and b/app/assets/images/emoji/family_wwgb.png differ
diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8d3f49b85fe81283461ad1dbb8f6018b3a2a5f7
Binary files /dev/null and b/app/assets/images/emoji/family_wwgg.png differ
diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png
new file mode 100644
index 0000000000000000000000000000000000000000..c406fedfdb12d94bc1b53baeac9a0895a54c9dc6
Binary files /dev/null and b/app/assets/images/emoji/fast_forward.png differ
diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f929e294c2622e308cec2d9c59018e51c5234fd
Binary files /dev/null and b/app/assets/images/emoji/fax.png differ
diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png
new file mode 100644
index 0000000000000000000000000000000000000000..eb8b347cef9350b9b6cd735bf45fa12e8f9d159f
Binary files /dev/null and b/app/assets/images/emoji/fearful.png differ
diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fe568cee93100723f1e3021040b6f09811ca127
Binary files /dev/null and b/app/assets/images/emoji/feet.png differ
diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png
new file mode 100644
index 0000000000000000000000000000000000000000..5288c920eb90a692ee25d0fe14c29d1714ea5fcb
Binary files /dev/null and b/app/assets/images/emoji/fencer.png differ
diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png
new file mode 100644
index 0000000000000000000000000000000000000000..55c8ff0475b593a66bb4473df5d8f1ecb74122ab
Binary files /dev/null and b/app/assets/images/emoji/ferris_wheel.png differ
diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png
new file mode 100644
index 0000000000000000000000000000000000000000..41816b3ae34069268889f6225fce298a9687823a
Binary files /dev/null and b/app/assets/images/emoji/ferry.png differ
diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png
new file mode 100644
index 0000000000000000000000000000000000000000..839637716ee8b97187a98d2d51214644844c20cb
Binary files /dev/null and b/app/assets/images/emoji/field_hockey.png differ
diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png
new file mode 100644
index 0000000000000000000000000000000000000000..fddc65dde96807d37fe6c97daa4273470757c016
Binary files /dev/null and b/app/assets/images/emoji/file_cabinet.png differ
diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png
new file mode 100644
index 0000000000000000000000000000000000000000..addedaf087061607e12f27b1d55805890dcae7cf
Binary files /dev/null and b/app/assets/images/emoji/file_folder.png differ
diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png
new file mode 100644
index 0000000000000000000000000000000000000000..30143aedbe69e11b9148b9c91424cf224c63c41a
Binary files /dev/null and b/app/assets/images/emoji/film_frames.png differ
diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png
new file mode 100644
index 0000000000000000000000000000000000000000..4cd18514ea316e6a64cb443125ee51bdcfed0aa2
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed.png differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd2384a6cd55c28a72712706e527e1474ddd53c7
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed_tone1.png differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6228401befe4bd791a3153623a0cfe03862229a5
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed_tone2.png differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1074da15f5d5921061fb52c4c160698ec282b0d
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed_tone3.png differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..75e05e4d332fa904982a7328d4f55d1cb9155c3b
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed_tone4.png differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..761aebdc30ff2fcf592739d4a8885d3ecd99cb29
Binary files /dev/null and b/app/assets/images/emoji/fingers_crossed_tone5.png differ
diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd3775a460bb0b1085b7de1c03841976a6bca778
Binary files /dev/null and b/app/assets/images/emoji/fire.png differ
diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png
new file mode 100644
index 0000000000000000000000000000000000000000..2cd45b7cf7e667ea3ccf2263927325562ebd4fbe
Binary files /dev/null and b/app/assets/images/emoji/fire_engine.png differ
diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png
new file mode 100644
index 0000000000000000000000000000000000000000..176c8b582659ebdfd49d183e22fdcdb5936b583c
Binary files /dev/null and b/app/assets/images/emoji/fireworks.png differ
diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png
new file mode 100644
index 0000000000000000000000000000000000000000..15612b66492e13fc8949f15df2ce7c182ff2281e
Binary files /dev/null and b/app/assets/images/emoji/first_place.png differ
diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dccaf72a4f59fdd7dd2305568bca0c8ab04a817
Binary files /dev/null and b/app/assets/images/emoji/first_quarter_moon.png differ
diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd8a3d7acd8c0420bafc370241edcd126ef4cdc3
Binary files /dev/null and b/app/assets/images/emoji/first_quarter_moon_with_face.png differ
diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..c2d2faaacd4fc76fc2cf29415ad651f013332e03
Binary files /dev/null and b/app/assets/images/emoji/fish.png differ
diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png
new file mode 100644
index 0000000000000000000000000000000000000000..157bded65dbd010d9e6295ada6b5bf05af4a3a26
Binary files /dev/null and b/app/assets/images/emoji/fish_cake.png differ
diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..dfcdf07eb50cc77f022478c3b5c95ccfeec2dcdf
Binary files /dev/null and b/app/assets/images/emoji/fishing_pole_and_fish.png differ
diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png
new file mode 100644
index 0000000000000000000000000000000000000000..de33592bf98e4c2a4c4e50882fb673d203f8fd7c
Binary files /dev/null and b/app/assets/images/emoji/fist.png differ
diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..02809e2dd68385f523b76ff7936d46c796729d0d
Binary files /dev/null and b/app/assets/images/emoji/fist_tone1.png differ
diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..5de34810383e8591833dea525afc222d6e0a1c32
Binary files /dev/null and b/app/assets/images/emoji/fist_tone2.png differ
diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d5240129b1c8c2a78160a7410992bd463ce6a36
Binary files /dev/null and b/app/assets/images/emoji/fist_tone3.png differ
diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a95c0dd634b82bf62c059d107d3dbc109a7649a6
Binary files /dev/null and b/app/assets/images/emoji/fist_tone4.png differ
diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2f092fd8c774ff0780cd1223537a995cb94f007
Binary files /dev/null and b/app/assets/images/emoji/fist_tone5.png differ
diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png
new file mode 100644
index 0000000000000000000000000000000000000000..d14371f3f27f01dc095b78a9b12be59bb6eb70ca
Binary files /dev/null and b/app/assets/images/emoji/five.png differ
diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png
new file mode 100644
index 0000000000000000000000000000000000000000..286239920c772b9135bcd0ac74e90712ef6cde48
Binary files /dev/null and b/app/assets/images/emoji/flag_ac.png differ
diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png
new file mode 100644
index 0000000000000000000000000000000000000000..20f4b14e8ad6908258ccd7c35cac323a77065957
Binary files /dev/null and b/app/assets/images/emoji/flag_ad.png differ
diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png
new file mode 100644
index 0000000000000000000000000000000000000000..d16ffe4b862fcad8e0add0158797b234d4927a4f
Binary files /dev/null and b/app/assets/images/emoji/flag_ae.png differ
diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png
new file mode 100644
index 0000000000000000000000000000000000000000..a51533b554d4b0eb28a4d0096c4b905c53c4cae0
Binary files /dev/null and b/app/assets/images/emoji/flag_af.png differ
diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png
new file mode 100644
index 0000000000000000000000000000000000000000..07f2ce397d09410189a2458a06e7d524d7c068cc
Binary files /dev/null and b/app/assets/images/emoji/flag_ag.png differ
diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png
new file mode 100644
index 0000000000000000000000000000000000000000..500b5ab09fbe0dd7d6376428c1f7bb8f7dbfe0c8
Binary files /dev/null and b/app/assets/images/emoji/flag_ai.png differ
diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png
new file mode 100644
index 0000000000000000000000000000000000000000..03a20132cc6f0900c1f18acc004aea3b14cbcd1d
Binary files /dev/null and b/app/assets/images/emoji/flag_al.png differ
diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ad60a273eca542b94f8f9b51f232c7279baee72
Binary files /dev/null and b/app/assets/images/emoji/flag_am.png differ
diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb46c31f86252f423a9c6335826bb7c98696b187
Binary files /dev/null and b/app/assets/images/emoji/flag_ao.png differ
diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png
new file mode 100644
index 0000000000000000000000000000000000000000..b272021d37566e02ebbe9762018b777651099d82
Binary files /dev/null and b/app/assets/images/emoji/flag_aq.png differ
diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png
new file mode 100644
index 0000000000000000000000000000000000000000..73136caf3b7ef37a44b7874dca6ea7832c82521e
Binary files /dev/null and b/app/assets/images/emoji/flag_ar.png differ
diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png
new file mode 100644
index 0000000000000000000000000000000000000000..3db45a0d9f39111fbd33b5343221ad7491ec3278
Binary files /dev/null and b/app/assets/images/emoji/flag_as.png differ
diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png
new file mode 100644
index 0000000000000000000000000000000000000000..c43769dcb193f5636749da86afc1021783481327
Binary files /dev/null and b/app/assets/images/emoji/flag_at.png differ
diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png
new file mode 100644
index 0000000000000000000000000000000000000000..7794309c78c3de7275df4f8bb14fc8de17f928b1
Binary files /dev/null and b/app/assets/images/emoji/flag_au.png differ
diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png
new file mode 100644
index 0000000000000000000000000000000000000000..02c840d12c90f68a1207d77d611a25b6c139fc5e
Binary files /dev/null and b/app/assets/images/emoji/flag_aw.png differ
diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc5466174bb52686e6160c8bd641679d3d465a55
Binary files /dev/null and b/app/assets/images/emoji/flag_ax.png differ
diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png
new file mode 100644
index 0000000000000000000000000000000000000000..89d3d15fd9fba4a9ba87e53d8c3e46fd94ce7d73
Binary files /dev/null and b/app/assets/images/emoji/flag_az.png differ
diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png
new file mode 100644
index 0000000000000000000000000000000000000000..25fe407e13c1ff3fa68d0a068f09e19c1f567350
Binary files /dev/null and b/app/assets/images/emoji/flag_ba.png differ
diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png
new file mode 100644
index 0000000000000000000000000000000000000000..bccd8c5c9b07e06edf6af02ceff33e99b39f7c53
Binary files /dev/null and b/app/assets/images/emoji/flag_bb.png differ
diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0597a3149b03deaedcee4a2914199e1f1190203
Binary files /dev/null and b/app/assets/images/emoji/flag_bd.png differ
diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png
new file mode 100644
index 0000000000000000000000000000000000000000..551f086e3c4b40f3b8d2f4bdb5f9dd6262b2ea0a
Binary files /dev/null and b/app/assets/images/emoji/flag_be.png differ
diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png
new file mode 100644
index 0000000000000000000000000000000000000000..444d4829f94eb44a4578426f3eb600cea3877da0
Binary files /dev/null and b/app/assets/images/emoji/flag_bf.png differ
diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png
new file mode 100644
index 0000000000000000000000000000000000000000..821eee5e170eb30bac313380c8eefc9937b63807
Binary files /dev/null and b/app/assets/images/emoji/flag_bg.png differ
diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png
new file mode 100644
index 0000000000000000000000000000000000000000..f33724249f086a80d192ce293cbda2908f5a0e4b
Binary files /dev/null and b/app/assets/images/emoji/flag_bh.png differ
diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea20ac93211f623757fc49775e72b43844a8b0ae
Binary files /dev/null and b/app/assets/images/emoji/flag_bi.png differ
diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cca4f8045798e76955976ce16613e0b7dcd7984
Binary files /dev/null and b/app/assets/images/emoji/flag_bj.png differ
diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png
new file mode 100644
index 0000000000000000000000000000000000000000..1082e78999f529418ee79c30b75e188769b76895
Binary files /dev/null and b/app/assets/images/emoji/flag_bl.png differ
diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e28d05d5acac9f4a47fc29c6fe1a6891e3a20f4
Binary files /dev/null and b/app/assets/images/emoji/flag_black.png differ
diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab8cafdac63211eaa194aec7bbccf1d4fe50acff
Binary files /dev/null and b/app/assets/images/emoji/flag_bm.png differ
diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png
new file mode 100644
index 0000000000000000000000000000000000000000..caa9329a8962d515076d23cddce1567080240aba
Binary files /dev/null and b/app/assets/images/emoji/flag_bn.png differ
diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png
new file mode 100644
index 0000000000000000000000000000000000000000..98af62b3da7d1d1def743010ec3b625ddd5bfab9
Binary files /dev/null and b/app/assets/images/emoji/flag_bo.png differ
diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb978ef9de917af2385680643be8d936c08be485
Binary files /dev/null and b/app/assets/images/emoji/flag_bq.png differ
diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png
new file mode 100644
index 0000000000000000000000000000000000000000..b139366a42bc6b5bbaaba7b3ef791faeb6a70357
Binary files /dev/null and b/app/assets/images/emoji/flag_br.png differ
diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png
new file mode 100644
index 0000000000000000000000000000000000000000..d36bcd2fb528074a699001c499a8e41a286db348
Binary files /dev/null and b/app/assets/images/emoji/flag_bs.png differ
diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed57aa0360ea29e346172c6f822743f594175af6
Binary files /dev/null and b/app/assets/images/emoji/flag_bt.png differ
diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png
new file mode 100644
index 0000000000000000000000000000000000000000..5884e648228c6839b1541a432b29c1de1661d0fb
Binary files /dev/null and b/app/assets/images/emoji/flag_bv.png differ
diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb12f34739dafd4854c97807f6c36ed7ba55d0dd
Binary files /dev/null and b/app/assets/images/emoji/flag_bw.png differ
diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png
new file mode 100644
index 0000000000000000000000000000000000000000..859c05beb13b2d4631e1a884dd82862748174f41
Binary files /dev/null and b/app/assets/images/emoji/flag_by.png differ
diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png
new file mode 100644
index 0000000000000000000000000000000000000000..34761cd03d844e643cacce35eb00c675f820f652
Binary files /dev/null and b/app/assets/images/emoji/flag_bz.png differ
diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c5b390e85b2952ba4b193a1736d98a5bd485dc5
Binary files /dev/null and b/app/assets/images/emoji/flag_ca.png differ
diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6555a23d839de9ce13b7de904c668b2dce50535
Binary files /dev/null and b/app/assets/images/emoji/flag_cc.png differ
diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa92009771d64d344cf1c802ab9b082cfacb274c
Binary files /dev/null and b/app/assets/images/emoji/flag_cd.png differ
diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png
new file mode 100644
index 0000000000000000000000000000000000000000..b969ae29ea9f17b68fb764954c031c11186fa732
Binary files /dev/null and b/app/assets/images/emoji/flag_cf.png differ
diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a38a40a95e0aaca3202562c7375634741679a92
Binary files /dev/null and b/app/assets/images/emoji/flag_cg.png differ
diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ff86b8a3b729b0f63eb66351a82b3a10905a0ca
Binary files /dev/null and b/app/assets/images/emoji/flag_ch.png differ
diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3b4d15c7f177fff8fa094b453765e320496760a
Binary files /dev/null and b/app/assets/images/emoji/flag_ci.png differ
diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6b53dbc1c4fef39f29b9cf4d3c612702a177816
Binary files /dev/null and b/app/assets/images/emoji/flag_ck.png differ
diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9390da5499402a5d441c4180be0a8cc4cb1ffa1
Binary files /dev/null and b/app/assets/images/emoji/flag_cl.png differ
diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png
new file mode 100644
index 0000000000000000000000000000000000000000..2d3f6ec45181da7d34b56748c8debfeffe2e4c55
Binary files /dev/null and b/app/assets/images/emoji/flag_cm.png differ
diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a7f350a6d2dcbadd5abd365a7f78529864181e0
Binary files /dev/null and b/app/assets/images/emoji/flag_cn.png differ
diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e0f5e0dc3cb2639072b3c3b6d8a4ea3170e2237
Binary files /dev/null and b/app/assets/images/emoji/flag_co.png differ
diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png
new file mode 100644
index 0000000000000000000000000000000000000000..70c761036bd30c485c8f456d5d3af95897114eb0
Binary files /dev/null and b/app/assets/images/emoji/flag_cp.png differ
diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5fce1265150a3430c5461d512dfe3e9a2dfd684
Binary files /dev/null and b/app/assets/images/emoji/flag_cr.png differ
diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png
new file mode 100644
index 0000000000000000000000000000000000000000..447328f7dfd7a01e81dc2a8bc1fc6a7117ba7337
Binary files /dev/null and b/app/assets/images/emoji/flag_cu.png differ
diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png
new file mode 100644
index 0000000000000000000000000000000000000000..43faf4d64d501c4e79f4d4fa9280f4be9512202c
Binary files /dev/null and b/app/assets/images/emoji/flag_cv.png differ
diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png
new file mode 100644
index 0000000000000000000000000000000000000000..eb39e8d0078c6004c7f0979d9625a1f72ba0e9d2
Binary files /dev/null and b/app/assets/images/emoji/flag_cw.png differ
diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png
new file mode 100644
index 0000000000000000000000000000000000000000..09d21359f3af73e86da65ca0426be2267917b2c0
Binary files /dev/null and b/app/assets/images/emoji/flag_cx.png differ
diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png
new file mode 100644
index 0000000000000000000000000000000000000000..154a7aa317632365361e75cacd7e35b5402a876d
Binary files /dev/null and b/app/assets/images/emoji/flag_cy.png differ
diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png
new file mode 100644
index 0000000000000000000000000000000000000000..9737ca223c7561a1be1f960b1a83daf7b09bcabc
Binary files /dev/null and b/app/assets/images/emoji/flag_cz.png differ
diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png
new file mode 100644
index 0000000000000000000000000000000000000000..98ed76b3bab7237a2b47907699a365d86d0ea6ab
Binary files /dev/null and b/app/assets/images/emoji/flag_de.png differ
diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png
new file mode 100644
index 0000000000000000000000000000000000000000..aae927d14b8bf08345f4f2373496c43f85dd0c55
Binary files /dev/null and b/app/assets/images/emoji/flag_dg.png differ
diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png
new file mode 100644
index 0000000000000000000000000000000000000000..73c2a2acbd98429da6506c28c4e96a260448145a
Binary files /dev/null and b/app/assets/images/emoji/flag_dj.png differ
diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5a60b06256fedcb89c4aa105c3dad7022857eab
Binary files /dev/null and b/app/assets/images/emoji/flag_dk.png differ
diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png
new file mode 100644
index 0000000000000000000000000000000000000000..50f8a53981d6b867fd29dbbc4d7954f8a970e815
Binary files /dev/null and b/app/assets/images/emoji/flag_dm.png differ
diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png
new file mode 100644
index 0000000000000000000000000000000000000000..037a45d7c269d141efb9724e2070b9c22ee4a870
Binary files /dev/null and b/app/assets/images/emoji/flag_do.png differ
diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png
new file mode 100644
index 0000000000000000000000000000000000000000..24945b10f2dfa0555a68982e9afb445a1e6c1249
Binary files /dev/null and b/app/assets/images/emoji/flag_dz.png differ
diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png
new file mode 100644
index 0000000000000000000000000000000000000000..356ff34783800d4ee24a5922c0a88c297b65a656
Binary files /dev/null and b/app/assets/images/emoji/flag_ea.png differ
diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png
new file mode 100644
index 0000000000000000000000000000000000000000..138145946199daa55f5037d56509473928172ece
Binary files /dev/null and b/app/assets/images/emoji/flag_ec.png differ
diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png
new file mode 100644
index 0000000000000000000000000000000000000000..84f317e77471d1123ba18a00968a5341b1861dd8
Binary files /dev/null and b/app/assets/images/emoji/flag_ee.png differ
diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png
new file mode 100644
index 0000000000000000000000000000000000000000..57786064a95f2be24eec5be2169ef8d35bf1fba8
Binary files /dev/null and b/app/assets/images/emoji/flag_eg.png differ
diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d7a76687f68dacf844aba88ca93e8435e7b766f
Binary files /dev/null and b/app/assets/images/emoji/flag_eh.png differ
diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c3c724c1fba9fbc0a51c7efc5547def8feabf53
Binary files /dev/null and b/app/assets/images/emoji/flag_er.png differ
diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e73597a2257a3be1dbaf38f727c0a828e451c8f
Binary files /dev/null and b/app/assets/images/emoji/flag_es.png differ
diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png
new file mode 100644
index 0000000000000000000000000000000000000000..9560a134c9706a6d946ba722bc72d4a5c8700fe4
Binary files /dev/null and b/app/assets/images/emoji/flag_et.png differ
diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b456cf33308b40732a3299db66d709dee6f1287
Binary files /dev/null and b/app/assets/images/emoji/flag_eu.png differ
diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png
new file mode 100644
index 0000000000000000000000000000000000000000..ebcf58abfc5a6cc08473ac21a7c1c008df745d68
Binary files /dev/null and b/app/assets/images/emoji/flag_fi.png differ
diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png
new file mode 100644
index 0000000000000000000000000000000000000000..9cc8c37fe3747ff09c441a73754669d18b3cd627
Binary files /dev/null and b/app/assets/images/emoji/flag_fj.png differ
diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png
new file mode 100644
index 0000000000000000000000000000000000000000..61372fd2549aadd233ad70579dbd1a7cf3c0c5b8
Binary files /dev/null and b/app/assets/images/emoji/flag_fk.png differ
diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png
new file mode 100644
index 0000000000000000000000000000000000000000..0889825c8e12e7ea259c8f79fbd6b5adfcf4f426
Binary files /dev/null and b/app/assets/images/emoji/flag_fm.png differ
diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a4431b0831b76ca835cb87bf533c3802cc9f521
Binary files /dev/null and b/app/assets/images/emoji/flag_fo.png differ
diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png
new file mode 100644
index 0000000000000000000000000000000000000000..62ca19c3fcf4a614eccd673d2cdf3d4af905663f
Binary files /dev/null and b/app/assets/images/emoji/flag_fr.png differ
diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e68e527a3ef502a25ac7fdcd14c509e505e40ef
Binary files /dev/null and b/app/assets/images/emoji/flag_ga.png differ
diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ed10f623470da5d41183c4d5f78029d40eebb0a
Binary files /dev/null and b/app/assets/images/emoji/flag_gb.png differ
diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png
new file mode 100644
index 0000000000000000000000000000000000000000..527aad338079a29c94b83a56c39fb3684539ac55
Binary files /dev/null and b/app/assets/images/emoji/flag_gd.png differ
diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png
new file mode 100644
index 0000000000000000000000000000000000000000..a75d142480db57883edba840309897b5f7f3659f
Binary files /dev/null and b/app/assets/images/emoji/flag_ge.png differ
diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png
new file mode 100644
index 0000000000000000000000000000000000000000..0cf96f327c000be2daa09bb3c15634ee6df24abb
Binary files /dev/null and b/app/assets/images/emoji/flag_gf.png differ
diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png
new file mode 100644
index 0000000000000000000000000000000000000000..970002c7f765eba6da38115018e35ae1595db8b8
Binary files /dev/null and b/app/assets/images/emoji/flag_gg.png differ
diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png
new file mode 100644
index 0000000000000000000000000000000000000000..f31b5eb7b45c185f2b66dcb89c5b154c3d7b413a
Binary files /dev/null and b/app/assets/images/emoji/flag_gh.png differ
diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png
new file mode 100644
index 0000000000000000000000000000000000000000..e554a2a1d0c6208310c76579f93a61d64e8bc8c8
Binary files /dev/null and b/app/assets/images/emoji/flag_gi.png differ
diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e795dd4e339dae76b1629d370a7df42dcad5d1b
Binary files /dev/null and b/app/assets/images/emoji/flag_gl.png differ
diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb69c0975a3e0ae257a94edc883b4222d3fcf7a7
Binary files /dev/null and b/app/assets/images/emoji/flag_gm.png differ
diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png
new file mode 100644
index 0000000000000000000000000000000000000000..1981f61dbf510b6e1f32c76f661d7bd6789da570
Binary files /dev/null and b/app/assets/images/emoji/flag_gn.png differ
diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png
new file mode 100644
index 0000000000000000000000000000000000000000..10e42e672bdcf1fce4e8b6ad21bfae526240f1a4
Binary files /dev/null and b/app/assets/images/emoji/flag_gp.png differ
diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png
new file mode 100644
index 0000000000000000000000000000000000000000..11475e61eeb05e7b757aaf56928f1838f58da6e0
Binary files /dev/null and b/app/assets/images/emoji/flag_gq.png differ
diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f6bb1b6b94bde157bf9f2e155e5297f7827ac7b
Binary files /dev/null and b/app/assets/images/emoji/flag_gr.png differ
diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fc92780453d1146d867149b60232b4aca55f6d2
Binary files /dev/null and b/app/assets/images/emoji/flag_gs.png differ
diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png
new file mode 100644
index 0000000000000000000000000000000000000000..7213d4139edddf68814616da48f1ba5adc4dfc41
Binary files /dev/null and b/app/assets/images/emoji/flag_gt.png differ
diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png
new file mode 100644
index 0000000000000000000000000000000000000000..4027549ca3cb40efc87b86ef2a2ecbe3c4c00557
Binary files /dev/null and b/app/assets/images/emoji/flag_gu.png differ
diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png
new file mode 100644
index 0000000000000000000000000000000000000000..6357f6225f4c57e1fa9fb3575896f64aa41c9dcb
Binary files /dev/null and b/app/assets/images/emoji/flag_gw.png differ
diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png
new file mode 100644
index 0000000000000000000000000000000000000000..746e2fb7e4475b857bb6bfecdcc769c109dbca83
Binary files /dev/null and b/app/assets/images/emoji/flag_gy.png differ
diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf0c7151b56546a4c34903f7148ec9bf415cd8aa
Binary files /dev/null and b/app/assets/images/emoji/flag_hk.png differ
diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png
new file mode 100644
index 0000000000000000000000000000000000000000..b613509e466797c6c55720bf9ce0d72c28234cad
Binary files /dev/null and b/app/assets/images/emoji/flag_hm.png differ
diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png
new file mode 100644
index 0000000000000000000000000000000000000000..402cdcefdf818de43341863a16472b7f01cefb5e
Binary files /dev/null and b/app/assets/images/emoji/flag_hn.png differ
diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png
new file mode 100644
index 0000000000000000000000000000000000000000..46f4f06b4f2f236d30003b214ebd6bb5dedde1fd
Binary files /dev/null and b/app/assets/images/emoji/flag_hr.png differ
diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8d0c8884980bafd8465718d91b21b06c082e906
Binary files /dev/null and b/app/assets/images/emoji/flag_ht.png differ
diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png
new file mode 100644
index 0000000000000000000000000000000000000000..a898de636a55b06b76f043a71b57d9f2eaacc172
Binary files /dev/null and b/app/assets/images/emoji/flag_hu.png differ
diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png
new file mode 100644
index 0000000000000000000000000000000000000000..69fd990aa95d90381c148585ea0af942b059ca76
Binary files /dev/null and b/app/assets/images/emoji/flag_ic.png differ
diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png
new file mode 100644
index 0000000000000000000000000000000000000000..85b4c063a45e09ee70135546a34040f29ee44adc
Binary files /dev/null and b/app/assets/images/emoji/flag_id.png differ
diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png
new file mode 100644
index 0000000000000000000000000000000000000000..a28295838cca408fe20dac4296925bb4003cb719
Binary files /dev/null and b/app/assets/images/emoji/flag_ie.png differ
diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png
new file mode 100644
index 0000000000000000000000000000000000000000..85c410d45fb14a821d7fafba904669ef0137dc66
Binary files /dev/null and b/app/assets/images/emoji/flag_il.png differ
diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png
new file mode 100644
index 0000000000000000000000000000000000000000..60a2458e38ec31ba54f892ab5e27eca038752311
Binary files /dev/null and b/app/assets/images/emoji/flag_im.png differ
diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png
new file mode 100644
index 0000000000000000000000000000000000000000..feccc8952ce3b6466aa700431636a50919c284bd
Binary files /dev/null and b/app/assets/images/emoji/flag_in.png differ
diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png
new file mode 100644
index 0000000000000000000000000000000000000000..aae927d14b8bf08345f4f2373496c43f85dd0c55
Binary files /dev/null and b/app/assets/images/emoji/flag_io.png differ
diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png
new file mode 100644
index 0000000000000000000000000000000000000000..41fd1db6f86dd35d28d01bb68106a0ac2a4035cf
Binary files /dev/null and b/app/assets/images/emoji/flag_iq.png differ
diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff7aaf62ba60479411b5ea27b99fe0287a1bf115
Binary files /dev/null and b/app/assets/images/emoji/flag_ir.png differ
diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad8d4131dd2213cae9c4645c1f77eb4f4fce9373
Binary files /dev/null and b/app/assets/images/emoji/flag_is.png differ
diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png
new file mode 100644
index 0000000000000000000000000000000000000000..f21563ec533eb71849c9415ca831ea682777ccdf
Binary files /dev/null and b/app/assets/images/emoji/flag_it.png differ
diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png
new file mode 100644
index 0000000000000000000000000000000000000000..198a918f6a468ddc1422c175946113e00a1f42f1
Binary files /dev/null and b/app/assets/images/emoji/flag_je.png differ
diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png
new file mode 100644
index 0000000000000000000000000000000000000000..f84e4f9e8dbd21c02832106c38f69966c7c137a5
Binary files /dev/null and b/app/assets/images/emoji/flag_jm.png differ
diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png
new file mode 100644
index 0000000000000000000000000000000000000000..20bfa147e3ee71036de7cc5996adfcac4bf60865
Binary files /dev/null and b/app/assets/images/emoji/flag_jo.png differ
diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d8838e4708a006f634607363ffae4caac8bd25e
Binary files /dev/null and b/app/assets/images/emoji/flag_jp.png differ
diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e417ab300965608a29da3990e6cc486278d4432
Binary files /dev/null and b/app/assets/images/emoji/flag_ke.png differ
diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f2d848fe5888b6bb33a89fdf59394a02947bfa3
Binary files /dev/null and b/app/assets/images/emoji/flag_kg.png differ
diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a2877dd620e8438385d0c8c6f3dd46dfd21972a
Binary files /dev/null and b/app/assets/images/emoji/flag_kh.png differ
diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png
new file mode 100644
index 0000000000000000000000000000000000000000..10e507e3245f095c6c8a5f7e1f2daf88f607f80b
Binary files /dev/null and b/app/assets/images/emoji/flag_ki.png differ
diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd5a0588e0321f73ef377e07fd4f770ae3f2b1c3
Binary files /dev/null and b/app/assets/images/emoji/flag_km.png differ
diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png
new file mode 100644
index 0000000000000000000000000000000000000000..776207c9605d5512a96ecf8f277e3888b4298af5
Binary files /dev/null and b/app/assets/images/emoji/flag_kn.png differ
diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b3fd89eaaabc2167a78c0c328f96f63b57414b1
Binary files /dev/null and b/app/assets/images/emoji/flag_kp.png differ
diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png
new file mode 100644
index 0000000000000000000000000000000000000000..833a88116e18216e0e76e710aef0408ffedf0306
Binary files /dev/null and b/app/assets/images/emoji/flag_kr.png differ
diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d19bfa6ca7a42986e096dcc46600c06528149a3
Binary files /dev/null and b/app/assets/images/emoji/flag_kw.png differ
diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png
new file mode 100644
index 0000000000000000000000000000000000000000..40daa4da597f1e4a54aa0dc3773d92dbddc1fe3c
Binary files /dev/null and b/app/assets/images/emoji/flag_ky.png differ
diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f97a8fd3c68d654275bfab89b85d94e7b31d078
Binary files /dev/null and b/app/assets/images/emoji/flag_kz.png differ
diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d4179f34f6be972e419484767b8409267fc01f9
Binary files /dev/null and b/app/assets/images/emoji/flag_la.png differ
diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d594467011bfe222d26f701475a45815f429008
Binary files /dev/null and b/app/assets/images/emoji/flag_lb.png differ
diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png
new file mode 100644
index 0000000000000000000000000000000000000000..45547b1e439eb518f8f389debb8956da89091dd3
Binary files /dev/null and b/app/assets/images/emoji/flag_lc.png differ
diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png
new file mode 100644
index 0000000000000000000000000000000000000000..0eafa6a2215aa679f93f0410ead08819b80862f3
Binary files /dev/null and b/app/assets/images/emoji/flag_li.png differ
diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab4fe10c40caa1ff9fff9d6ec5cbafe2eddb0d41
Binary files /dev/null and b/app/assets/images/emoji/flag_lk.png differ
diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png
new file mode 100644
index 0000000000000000000000000000000000000000..f66f267fea2e2d00054fb55e78d2cf9e6f822d38
Binary files /dev/null and b/app/assets/images/emoji/flag_lr.png differ
diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png
new file mode 100644
index 0000000000000000000000000000000000000000..24745631e3c70e2146a2fbf9f959ef3e7350ed0f
Binary files /dev/null and b/app/assets/images/emoji/flag_ls.png differ
diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png
new file mode 100644
index 0000000000000000000000000000000000000000..d644b56d62a7fd8e2c955d3d30469a9a917adf69
Binary files /dev/null and b/app/assets/images/emoji/flag_lt.png differ
diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2df9c929949f747ebab0b4b57eea06e6da11e13
Binary files /dev/null and b/app/assets/images/emoji/flag_lu.png differ
diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae680d5f0e3509cdd97539775207bcf4eb81bf0b
Binary files /dev/null and b/app/assets/images/emoji/flag_lv.png differ
diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6e77b0f3ba7df28757c83926c9f948f845bfd59
Binary files /dev/null and b/app/assets/images/emoji/flag_ly.png differ
diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4a056722cd3cc4af4e034c71330ab7a2d4486a2
Binary files /dev/null and b/app/assets/images/emoji/flag_ma.png differ
diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png
new file mode 100644
index 0000000000000000000000000000000000000000..d479eab98cb97af18db6bffa63b18567a5431dc3
Binary files /dev/null and b/app/assets/images/emoji/flag_mc.png differ
diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7a72539872886ef3308f2f883e5b8ca5f908362
Binary files /dev/null and b/app/assets/images/emoji/flag_md.png differ
diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c771e7e120312cc108aa78400942950e07854fa
Binary files /dev/null and b/app/assets/images/emoji/flag_me.png differ
diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png
new file mode 100644
index 0000000000000000000000000000000000000000..70c761036bd30c485c8f456d5d3af95897114eb0
Binary files /dev/null and b/app/assets/images/emoji/flag_mf.png differ
diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f3ccdda76fa563e20dcf1f1707769fbed4f1216
Binary files /dev/null and b/app/assets/images/emoji/flag_mg.png differ
diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png
new file mode 100644
index 0000000000000000000000000000000000000000..598016481c1916d66e5b3eb60cb2690b3719f379
Binary files /dev/null and b/app/assets/images/emoji/flag_mh.png differ
diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ba775ee75c0afdcc55a586b329238b658046422
Binary files /dev/null and b/app/assets/images/emoji/flag_mk.png differ
diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png
new file mode 100644
index 0000000000000000000000000000000000000000..6834378546809c3bb39d479f32829d364151b5e1
Binary files /dev/null and b/app/assets/images/emoji/flag_ml.png differ
diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png
new file mode 100644
index 0000000000000000000000000000000000000000..37dc7d715912f6a7259ee17d910d1d5db834a7cc
Binary files /dev/null and b/app/assets/images/emoji/flag_mm.png differ
diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f146bbcd1a60d6ac3324c92ed0ec6477317abe0
Binary files /dev/null and b/app/assets/images/emoji/flag_mn.png differ
diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png
new file mode 100644
index 0000000000000000000000000000000000000000..7edde31f64bd0727219af3ce4a5f0c49b0cd8d89
Binary files /dev/null and b/app/assets/images/emoji/flag_mo.png differ
diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png
new file mode 100644
index 0000000000000000000000000000000000000000..17ec1c441ed792f89afae38eaa7c9ee6cf7962e5
Binary files /dev/null and b/app/assets/images/emoji/flag_mp.png differ
diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e672dc90871036b107724e55235b8a9af4cfbf4
Binary files /dev/null and b/app/assets/images/emoji/flag_mq.png differ
diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png
new file mode 100644
index 0000000000000000000000000000000000000000..f87de46effebdad80884bcf2c70ed1109329f7ac
Binary files /dev/null and b/app/assets/images/emoji/flag_mr.png differ
diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png
new file mode 100644
index 0000000000000000000000000000000000000000..480b0d4ebda095b03630126e5b19485ff204da25
Binary files /dev/null and b/app/assets/images/emoji/flag_ms.png differ
diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9e1dbdce822605fab40c15618bc33d6c042899c
Binary files /dev/null and b/app/assets/images/emoji/flag_mt.png differ
diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png
new file mode 100644
index 0000000000000000000000000000000000000000..55b33cb7c33eefea0707600ba05b65d19a29ed99
Binary files /dev/null and b/app/assets/images/emoji/flag_mu.png differ
diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce5867126ae48f6861d01da19b5cbcab063d92af
Binary files /dev/null and b/app/assets/images/emoji/flag_mv.png differ
diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png
new file mode 100644
index 0000000000000000000000000000000000000000..003d85484014365956e250e801614ec35de72ec3
Binary files /dev/null and b/app/assets/images/emoji/flag_mw.png differ
diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png
new file mode 100644
index 0000000000000000000000000000000000000000..42572bcd0ba0694291886bb62a7ff8fd7f56bf79
Binary files /dev/null and b/app/assets/images/emoji/flag_mx.png differ
diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png
new file mode 100644
index 0000000000000000000000000000000000000000..17526c26742967201caf6cbf5172d7abd36527d1
Binary files /dev/null and b/app/assets/images/emoji/flag_my.png differ
diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png
new file mode 100644
index 0000000000000000000000000000000000000000..2352a78e786ff4bc53c9e6ee1571b699feb60188
Binary files /dev/null and b/app/assets/images/emoji/flag_mz.png differ
diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed31c3df04d09dcb35f19cc8cf582e1a7102df12
Binary files /dev/null and b/app/assets/images/emoji/flag_na.png differ
diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png
new file mode 100644
index 0000000000000000000000000000000000000000..90b3afebfa3c3eae7c442600b39bf97848bf7bbe
Binary files /dev/null and b/app/assets/images/emoji/flag_nc.png differ
diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png
new file mode 100644
index 0000000000000000000000000000000000000000..f98a1173c2a918888c45e2386aa7ffcfe8d9381d
Binary files /dev/null and b/app/assets/images/emoji/flag_ne.png differ
diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png
new file mode 100644
index 0000000000000000000000000000000000000000..9099e767420313fe80feeff4692cd181ea3a0097
Binary files /dev/null and b/app/assets/images/emoji/flag_nf.png differ
diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea0abeff1a1404eb38768c460159d1462ca15a02
Binary files /dev/null and b/app/assets/images/emoji/flag_ng.png differ
diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png
new file mode 100644
index 0000000000000000000000000000000000000000..772920dfa10d9dc8f7f5baa26065a865e97e6c6b
Binary files /dev/null and b/app/assets/images/emoji/flag_ni.png differ
diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png
new file mode 100644
index 0000000000000000000000000000000000000000..83a0e817e41d3b44c2fc3213011678c1b1ce67d0
Binary files /dev/null and b/app/assets/images/emoji/flag_nl.png differ
diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png
new file mode 100644
index 0000000000000000000000000000000000000000..99d3142eb7b1a2e3f25bcb0535035328bfa094a6
Binary files /dev/null and b/app/assets/images/emoji/flag_no.png differ
diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png
new file mode 100644
index 0000000000000000000000000000000000000000..87425a8dfef4fac5745033fb7eb219d0fe71a6b6
Binary files /dev/null and b/app/assets/images/emoji/flag_np.png differ
diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3e3a5d56215bbe40842913c6ed027ec950d0af6
Binary files /dev/null and b/app/assets/images/emoji/flag_nr.png differ
diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png
new file mode 100644
index 0000000000000000000000000000000000000000..f03614443ee9cc659a323b86a040dc0b44da9067
Binary files /dev/null and b/app/assets/images/emoji/flag_nu.png differ
diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4eeeab9cd93ad21bdbd8713484a20d40de49008
Binary files /dev/null and b/app/assets/images/emoji/flag_nz.png differ
diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea824ba31e7d6fba70e851a69c6cf6ff5d5789c4
Binary files /dev/null and b/app/assets/images/emoji/flag_om.png differ
diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3091d89889135cb464a3323d24803d8d798004f
Binary files /dev/null and b/app/assets/images/emoji/flag_pa.png differ
diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png
new file mode 100644
index 0000000000000000000000000000000000000000..39223aa9dbb8e879d9e4ec7e10f92a8125fc560b
Binary files /dev/null and b/app/assets/images/emoji/flag_pe.png differ
diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png
new file mode 100644
index 0000000000000000000000000000000000000000..113445f8f6e47a305f3feff093991e6564dc123a
Binary files /dev/null and b/app/assets/images/emoji/flag_pf.png differ
diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png
new file mode 100644
index 0000000000000000000000000000000000000000..825e9dcb762bd16e2379008db57926478d6de289
Binary files /dev/null and b/app/assets/images/emoji/flag_pg.png differ
diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png
new file mode 100644
index 0000000000000000000000000000000000000000..8260e15bd2ca0f03b499a4f2293599b43d97fe94
Binary files /dev/null and b/app/assets/images/emoji/flag_ph.png differ
diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7b6a1c5074209f3be45f6cd110c433cc797f707
Binary files /dev/null and b/app/assets/images/emoji/flag_pk.png differ
diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png
new file mode 100644
index 0000000000000000000000000000000000000000..19de2edec1177b91b8f7bb66ea78c4a202d3f54f
Binary files /dev/null and b/app/assets/images/emoji/flag_pl.png differ
diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ca60554193525e9c3e5518652f079d90e5066f5
Binary files /dev/null and b/app/assets/images/emoji/flag_pm.png differ
diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2263b154bc10da00916f5b643f209c0252509f8
Binary files /dev/null and b/app/assets/images/emoji/flag_pn.png differ
diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0209cddb7916f379f85092cfccebb6f358535fc
Binary files /dev/null and b/app/assets/images/emoji/flag_pr.png differ
diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ccab09778ba9eba08537c5652c169c1af7c69d4
Binary files /dev/null and b/app/assets/images/emoji/flag_ps.png differ
diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc93f27c64baba5ed011c67c51518b14a43af6bd
Binary files /dev/null and b/app/assets/images/emoji/flag_pt.png differ
diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png
new file mode 100644
index 0000000000000000000000000000000000000000..154b2f12d3cbb5688c2924e5784df6ae9eff2cba
Binary files /dev/null and b/app/assets/images/emoji/flag_pw.png differ
diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png
new file mode 100644
index 0000000000000000000000000000000000000000..662ad2f6ff1ac64b414b53d0cfc6f8dbc2ad1e85
Binary files /dev/null and b/app/assets/images/emoji/flag_py.png differ
diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png
new file mode 100644
index 0000000000000000000000000000000000000000..a01d8b05cc7698de1de7f728bf0dae80147018cc
Binary files /dev/null and b/app/assets/images/emoji/flag_qa.png differ
diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png
new file mode 100644
index 0000000000000000000000000000000000000000..57f2bbe9df8c977d65e985bbc23cd7b26d035c80
Binary files /dev/null and b/app/assets/images/emoji/flag_re.png differ
diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e48c447706f7c321ad41496ef956bbe986256b7
Binary files /dev/null and b/app/assets/images/emoji/flag_ro.png differ
diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png
new file mode 100644
index 0000000000000000000000000000000000000000..9df6c9a5235eab02a527e5ea8079929a19535f74
Binary files /dev/null and b/app/assets/images/emoji/flag_rs.png differ
diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png
new file mode 100644
index 0000000000000000000000000000000000000000..e50c9db90e72c163e6a3fbf120023fc88f821642
Binary files /dev/null and b/app/assets/images/emoji/flag_ru.png differ
diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png
new file mode 100644
index 0000000000000000000000000000000000000000..c238c874e1d8d5d871cf70d5533acad6efb2c5dd
Binary files /dev/null and b/app/assets/images/emoji/flag_rw.png differ
diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png
new file mode 100644
index 0000000000000000000000000000000000000000..4941be7d198b2538224920499cce3929436016c8
Binary files /dev/null and b/app/assets/images/emoji/flag_sa.png differ
diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d8f1ac6130fe1bf26e0f715b1cd07cb1e1d0e55
Binary files /dev/null and b/app/assets/images/emoji/flag_sb.png differ
diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ae4d90765eea92721ed0b4faf2c457df9f8f37a
Binary files /dev/null and b/app/assets/images/emoji/flag_sc.png differ
diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png
new file mode 100644
index 0000000000000000000000000000000000000000..963be1b36fbee1c7004bfff2d0cfa0f2e2b57365
Binary files /dev/null and b/app/assets/images/emoji/flag_sd.png differ
diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc0d0e0ce8987c7b3c7316dc69d162c7ef87b090
Binary files /dev/null and b/app/assets/images/emoji/flag_se.png differ
diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png
new file mode 100644
index 0000000000000000000000000000000000000000..de3c7737c425c9fdbb7dad5e72a4b8a4e29f2771
Binary files /dev/null and b/app/assets/images/emoji/flag_sg.png differ
diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png
new file mode 100644
index 0000000000000000000000000000000000000000..40cd9e44e962eaf4521c1a74ef403f4920602538
Binary files /dev/null and b/app/assets/images/emoji/flag_sh.png differ
diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png
new file mode 100644
index 0000000000000000000000000000000000000000..e308999dba2485e7e0354c86b7ac6617f9620bd0
Binary files /dev/null and b/app/assets/images/emoji/flag_si.png differ
diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png
new file mode 100644
index 0000000000000000000000000000000000000000..5884e648228c6839b1541a432b29c1de1661d0fb
Binary files /dev/null and b/app/assets/images/emoji/flag_sj.png differ
diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png
new file mode 100644
index 0000000000000000000000000000000000000000..4259d0e1418cb8559476a1034077444adb7355ed
Binary files /dev/null and b/app/assets/images/emoji/flag_sk.png differ
diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2cc68830ab4c50892848eb7e769f4231c4f51b9
Binary files /dev/null and b/app/assets/images/emoji/flag_sl.png differ
diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png
new file mode 100644
index 0000000000000000000000000000000000000000..03b8708754e27e2c223502d69506b1d27019e683
Binary files /dev/null and b/app/assets/images/emoji/flag_sm.png differ
diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png
new file mode 100644
index 0000000000000000000000000000000000000000..5368bbe93df519591ee053c23de2ed2ce3b05b7b
Binary files /dev/null and b/app/assets/images/emoji/flag_sn.png differ
diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png
new file mode 100644
index 0000000000000000000000000000000000000000..68a0597365a49c4b1f2ed7d3019d3292ab0b76d0
Binary files /dev/null and b/app/assets/images/emoji/flag_so.png differ
diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png
new file mode 100644
index 0000000000000000000000000000000000000000..d325132703570d460eca4f889dc90f139d23514d
Binary files /dev/null and b/app/assets/images/emoji/flag_sr.png differ
diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png
new file mode 100644
index 0000000000000000000000000000000000000000..122977e798f17d76b45fcd6afe8231b4dbc94584
Binary files /dev/null and b/app/assets/images/emoji/flag_ss.png differ
diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png
new file mode 100644
index 0000000000000000000000000000000000000000..f83a863d612ebf1eaf692c438021df1d0704949b
Binary files /dev/null and b/app/assets/images/emoji/flag_st.png differ
diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png
new file mode 100644
index 0000000000000000000000000000000000000000..efb83e2f253f1f55fe31c0f84d5b4f610269afc8
Binary files /dev/null and b/app/assets/images/emoji/flag_sv.png differ
diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png
new file mode 100644
index 0000000000000000000000000000000000000000..94b760fbedffce769deeafc6fc73f28158c41ed9
Binary files /dev/null and b/app/assets/images/emoji/flag_sx.png differ
diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png
new file mode 100644
index 0000000000000000000000000000000000000000..09a8ee8f78ceedf8ba5a13a02ccc89a3e6328c3d
Binary files /dev/null and b/app/assets/images/emoji/flag_sy.png differ
diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png
new file mode 100644
index 0000000000000000000000000000000000000000..f74e82ea1fdeb08509a4d130fd0d657ffff51b5f
Binary files /dev/null and b/app/assets/images/emoji/flag_sz.png differ
diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png
new file mode 100644
index 0000000000000000000000000000000000000000..b44283e90e29bd262dc624babb3e0ade9c9311c6
Binary files /dev/null and b/app/assets/images/emoji/flag_ta.png differ
diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png
new file mode 100644
index 0000000000000000000000000000000000000000..156b33d1ba6487496fe5157b563b587b568aa290
Binary files /dev/null and b/app/assets/images/emoji/flag_tc.png differ
diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png
new file mode 100644
index 0000000000000000000000000000000000000000..ebe7f59282832acc83c8499c0e5d111c57cf4e51
Binary files /dev/null and b/app/assets/images/emoji/flag_td.png differ
diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1a3ad68ee28c665873e8668ded0a42be09a4b73
Binary files /dev/null and b/app/assets/images/emoji/flag_tf.png differ
diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png
new file mode 100644
index 0000000000000000000000000000000000000000..826b73c9ac519de79d25054874f74d813303798a
Binary files /dev/null and b/app/assets/images/emoji/flag_tg.png differ
diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png
new file mode 100644
index 0000000000000000000000000000000000000000..93ff542c5a61452afa4a09f0e13cd96adb491c85
Binary files /dev/null and b/app/assets/images/emoji/flag_th.png differ
diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a8a0b6190a5d751aacd3a3f5614e5af96c5282e
Binary files /dev/null and b/app/assets/images/emoji/flag_tj.png differ
diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fa5a21b1bb2d434271fc139b078fe60efe0a824
Binary files /dev/null and b/app/assets/images/emoji/flag_tk.png differ
diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b120eccc6f79d21b5d9977951bd5e68ed1f9d30
Binary files /dev/null and b/app/assets/images/emoji/flag_tl.png differ
diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3c4f53230274fab93a654827cf7c627df578ef1
Binary files /dev/null and b/app/assets/images/emoji/flag_tm.png differ
diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png
new file mode 100644
index 0000000000000000000000000000000000000000..58ef161229fe77513a1c7c392218b96e216806d4
Binary files /dev/null and b/app/assets/images/emoji/flag_tn.png differ
diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ffa7bb9d191bff7f3b8e755712efa210689f99f
Binary files /dev/null and b/app/assets/images/emoji/flag_to.png differ
diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png
new file mode 100644
index 0000000000000000000000000000000000000000..325251fae8849773461a64f24c321cb70c72b418
Binary files /dev/null and b/app/assets/images/emoji/flag_tr.png differ
diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed3bb39a3001f3a7211146a7b0027161dedc7cca
Binary files /dev/null and b/app/assets/images/emoji/flag_tt.png differ
diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png
new file mode 100644
index 0000000000000000000000000000000000000000..e82c65c7bb980e9f6223b04f48c512782b432da5
Binary files /dev/null and b/app/assets/images/emoji/flag_tv.png differ
diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a8f00b592886acc9935d696394e89ca0e0ff882
Binary files /dev/null and b/app/assets/images/emoji/flag_tw.png differ
diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a020853d4edcf2946de406321875db0d49d4788
Binary files /dev/null and b/app/assets/images/emoji/flag_tz.png differ
diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd84d1bbd36ce5788e9442be03f59630a8bb922c
Binary files /dev/null and b/app/assets/images/emoji/flag_ua.png differ
diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc97690eb55f08402a96b8682806f384b0158b5d
Binary files /dev/null and b/app/assets/images/emoji/flag_ug.png differ
diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a7ee3cdf13c94f09e1ec9fc732a2e230407d5a9
Binary files /dev/null and b/app/assets/images/emoji/flag_um.png differ
diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f730305860c71e74ef4adbf77ea523e16efd28e
Binary files /dev/null and b/app/assets/images/emoji/flag_us.png differ
diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8002a697a6a44938ccd532033c101ec38cbb27d
Binary files /dev/null and b/app/assets/images/emoji/flag_uy.png differ
diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png
new file mode 100644
index 0000000000000000000000000000000000000000..d56ca9bc4248493dd67d20400208c774d20a9ccb
Binary files /dev/null and b/app/assets/images/emoji/flag_uz.png differ
diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png
new file mode 100644
index 0000000000000000000000000000000000000000..ddaf5e3141bfb50df760292bd2ba04ca5990ca80
Binary files /dev/null and b/app/assets/images/emoji/flag_va.png differ
diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png
new file mode 100644
index 0000000000000000000000000000000000000000..43703c62a716aa3fe8b016c9740cd75d32d59164
Binary files /dev/null and b/app/assets/images/emoji/flag_vc.png differ
diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b62796824ecd8aa667b0fb14dcc7d5c6f33f9b5
Binary files /dev/null and b/app/assets/images/emoji/flag_ve.png differ
diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png
new file mode 100644
index 0000000000000000000000000000000000000000..536f780f1c025f817cc3adb7d23a49a80c105172
Binary files /dev/null and b/app/assets/images/emoji/flag_vg.png differ
diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png
new file mode 100644
index 0000000000000000000000000000000000000000..64102012cfec7c372284f9e88a6629ff7e082d95
Binary files /dev/null and b/app/assets/images/emoji/flag_vi.png differ
diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png
new file mode 100644
index 0000000000000000000000000000000000000000..427036046b667ece5a9a915eaef854ed165e0f36
Binary files /dev/null and b/app/assets/images/emoji/flag_vn.png differ
diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png
new file mode 100644
index 0000000000000000000000000000000000000000..706eba440709c1104371b15afee8de42d6b5c045
Binary files /dev/null and b/app/assets/images/emoji/flag_vu.png differ
diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png
new file mode 100644
index 0000000000000000000000000000000000000000..70c761036bd30c485c8f456d5d3af95897114eb0
Binary files /dev/null and b/app/assets/images/emoji/flag_wf.png differ
diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png
new file mode 100644
index 0000000000000000000000000000000000000000..86d6e96d5e9ec31c1a716aac7ebd6252f1a4f58a
Binary files /dev/null and b/app/assets/images/emoji/flag_white.png differ
diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1ea07031418d13cc86084cf4f7a0d69ee315169
Binary files /dev/null and b/app/assets/images/emoji/flag_ws.png differ
diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png
new file mode 100644
index 0000000000000000000000000000000000000000..e587a446632fd2e7862f5c8b60bd960f542ce6e5
Binary files /dev/null and b/app/assets/images/emoji/flag_xk.png differ
diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png
new file mode 100644
index 0000000000000000000000000000000000000000..eadfebd5f67539e8fea33a30e7e0608f6a3a8248
Binary files /dev/null and b/app/assets/images/emoji/flag_ye.png differ
diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png
new file mode 100644
index 0000000000000000000000000000000000000000..c81fa6d886edf8abd7017f459ef10fcca6ddcbb7
Binary files /dev/null and b/app/assets/images/emoji/flag_yt.png differ
diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png
new file mode 100644
index 0000000000000000000000000000000000000000..f397ef5072fc76ee6a2787dd7aded8975aa8ffc8
Binary files /dev/null and b/app/assets/images/emoji/flag_za.png differ
diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png
new file mode 100644
index 0000000000000000000000000000000000000000..2494a31f662fdc6a78d34cc8d9316190370ab1a4
Binary files /dev/null and b/app/assets/images/emoji/flag_zm.png differ
diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png
new file mode 100644
index 0000000000000000000000000000000000000000..e09b9652be632dfd1af1456c8bb9930762066a95
Binary files /dev/null and b/app/assets/images/emoji/flag_zw.png differ
diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b451035a3a469f1e954151f5b2def2132cfdb5d
Binary files /dev/null and b/app/assets/images/emoji/flags.png differ
diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png
new file mode 100644
index 0000000000000000000000000000000000000000..eee36c250673fdddafd411c8dec15dffac4a7582
Binary files /dev/null and b/app/assets/images/emoji/flashlight.png differ
diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9250d27fa77678baaeed3b9d78ebd259b360cc5
Binary files /dev/null and b/app/assets/images/emoji/fleur-de-lis.png differ
diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png
new file mode 100644
index 0000000000000000000000000000000000000000..072a76d3c138b289f845cd4e5891559d251bd0d8
Binary files /dev/null and b/app/assets/images/emoji/floppy_disk.png differ
diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png
new file mode 100644
index 0000000000000000000000000000000000000000..6766b044d9599a6d21ed3c50e3b356b39ecd8c59
Binary files /dev/null and b/app/assets/images/emoji/flower_playing_cards.png differ
diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png
new file mode 100644
index 0000000000000000000000000000000000000000..829220bc470e3cf1e1484b3c6637f5b1fb309831
Binary files /dev/null and b/app/assets/images/emoji/flushed.png differ
diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e73c2de2728bd011f280bc4afb6e52094cd93e2
Binary files /dev/null and b/app/assets/images/emoji/fog.png differ
diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png
new file mode 100644
index 0000000000000000000000000000000000000000..57702d8d3acc9ee80f1652a389407807af46a481
Binary files /dev/null and b/app/assets/images/emoji/foggy.png differ
diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png
new file mode 100644
index 0000000000000000000000000000000000000000..10366f41fce338eb0b2da10564b1c1203f4e5da1
Binary files /dev/null and b/app/assets/images/emoji/football.png differ
diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2673c5a1a8f17979f0994011de3ef66bafe8c44
Binary files /dev/null and b/app/assets/images/emoji/footprints.png differ
diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png
new file mode 100644
index 0000000000000000000000000000000000000000..09f1feaea1c857938ade779dec58cc85b67acf2d
Binary files /dev/null and b/app/assets/images/emoji/fork_and_knife.png differ
diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png
new file mode 100644
index 0000000000000000000000000000000000000000..7411755f7087263dd29e4dd7e859b6c7a70bd179
Binary files /dev/null and b/app/assets/images/emoji/fork_knife_plate.png differ
diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png
new file mode 100644
index 0000000000000000000000000000000000000000..293f5d91c0fc22401f345602e9163dfb00d58446
Binary files /dev/null and b/app/assets/images/emoji/fountain.png differ
diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0e914aac457dba62eb0796951c6c8c6cbc85701
Binary files /dev/null and b/app/assets/images/emoji/four.png differ
diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdedfcc2b4ec853b2f341313db3fade7e4fb3132
Binary files /dev/null and b/app/assets/images/emoji/four_leaf_clover.png differ
diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ab339bf054298899cf7964309ad64a81297497a
Binary files /dev/null and b/app/assets/images/emoji/fox.png differ
diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fe84607bfdb70e02cd97cda8a23bb5c6c4776b2
Binary files /dev/null and b/app/assets/images/emoji/frame_photo.png differ
diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png
new file mode 100644
index 0000000000000000000000000000000000000000..b71956eb48af0b3d4480190598bed0001bfd5c84
Binary files /dev/null and b/app/assets/images/emoji/free.png differ
diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png
new file mode 100644
index 0000000000000000000000000000000000000000..4c2c563982226a4a7a0158c78216974f1489a356
Binary files /dev/null and b/app/assets/images/emoji/french_bread.png differ
diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png
new file mode 100644
index 0000000000000000000000000000000000000000..752ba7f1398a4a81453cce4f05e462ed8580d829
Binary files /dev/null and b/app/assets/images/emoji/fried_shrimp.png differ
diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e2a4caacefb691f3368e7f08bbd539c8ca250ec
Binary files /dev/null and b/app/assets/images/emoji/fries.png differ
diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png
new file mode 100644
index 0000000000000000000000000000000000000000..8825d1ad5774b84397800fcee39faef0d6272225
Binary files /dev/null and b/app/assets/images/emoji/frog.png differ
diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png
new file mode 100644
index 0000000000000000000000000000000000000000..43ab6b0a1c1186f1010f4fef7a655164dc139aa2
Binary files /dev/null and b/app/assets/images/emoji/frowning.png differ
diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ae71f233b9d8f9c9ae92074003160aea441a01e
Binary files /dev/null and b/app/assets/images/emoji/frowning2.png differ
diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png
new file mode 100644
index 0000000000000000000000000000000000000000..05b1879447471b0930949351a87fd70772a72a89
Binary files /dev/null and b/app/assets/images/emoji/fuelpump.png differ
diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9a2d6aa7c9c7a2ef56ff1960a5ed9d9669d138d
Binary files /dev/null and b/app/assets/images/emoji/full_moon.png differ
diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5c25bbaf64cbedc4f57aed1bb7bebfe1afc530f
Binary files /dev/null and b/app/assets/images/emoji/full_moon_with_face.png differ
diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad3626fe5e5fa07afc98e6f33a85e6fa371c07eb
Binary files /dev/null and b/app/assets/images/emoji/game_die.png differ
diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a1cc2c0ff496c14e8292c9907ceec6d24a98668
Binary files /dev/null and b/app/assets/images/emoji/gear.png differ
diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png
new file mode 100644
index 0000000000000000000000000000000000000000..db122d26a19a440da38fcd5997191c8918857a90
Binary files /dev/null and b/app/assets/images/emoji/gem.png differ
diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a09698cf00e1c966c59feb5b658f7c6d0626415
Binary files /dev/null and b/app/assets/images/emoji/gemini.png differ
diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png
new file mode 100644
index 0000000000000000000000000000000000000000..5650bc0ed180fa5152f28f2d607c20e182c8a3fa
Binary files /dev/null and b/app/assets/images/emoji/ghost.png differ
diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png
new file mode 100644
index 0000000000000000000000000000000000000000..844e2164560dbbb950255924ca173350c580d0be
Binary files /dev/null and b/app/assets/images/emoji/gift.png differ
diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..902ceafe4d1e45eaeaadaf43e37baee32c642d40
Binary files /dev/null and b/app/assets/images/emoji/gift_heart.png differ
diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc1d4d08b390ecbf390b3a10b0d5cafccb5dd3bc
Binary files /dev/null and b/app/assets/images/emoji/girl.png differ
diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb667e886515fa3d6b9e6f87ff4b00fb4f1042ca
Binary files /dev/null and b/app/assets/images/emoji/girl_tone1.png differ
diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..a59ed4a3f0d792b1164dad652439ec101d93518c
Binary files /dev/null and b/app/assets/images/emoji/girl_tone2.png differ
diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..517e7f2a7b0d5a40230548cd1d3132054a44c9f4
Binary files /dev/null and b/app/assets/images/emoji/girl_tone3.png differ
diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..542d96c848757979e3790ffb2cfdaaacda318718
Binary files /dev/null and b/app/assets/images/emoji/girl_tone4.png differ
diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..66b7c28c2dfe85b14dfdf3d9873168077878044b
Binary files /dev/null and b/app/assets/images/emoji/girl_tone5.png differ
diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png
new file mode 100644
index 0000000000000000000000000000000000000000..82450c1a4ba755860874f2707390b678f3bbf291
Binary files /dev/null and b/app/assets/images/emoji/globe_with_meridians.png differ
diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png
new file mode 100644
index 0000000000000000000000000000000000000000..df3a53da0fbf8859af3d84237d619f0c98861a9c
Binary files /dev/null and b/app/assets/images/emoji/goal.png differ
diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9d9e38a12872b9902d8a58ca07889ec71e2a959
Binary files /dev/null and b/app/assets/images/emoji/goat.png differ
diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png
new file mode 100644
index 0000000000000000000000000000000000000000..f65a21d8a46c08074ad9b71ad1ef85530360d912
Binary files /dev/null and b/app/assets/images/emoji/golf.png differ
diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png
new file mode 100644
index 0000000000000000000000000000000000000000..39c552de86da108925a95a89d86b59c2a8f7e7ec
Binary files /dev/null and b/app/assets/images/emoji/golfer.png differ
diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png
new file mode 100644
index 0000000000000000000000000000000000000000..acc51e13622aec5a1fa8d783607cb1ad010a6d95
Binary files /dev/null and b/app/assets/images/emoji/gorilla.png differ
diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png
new file mode 100644
index 0000000000000000000000000000000000000000..30d22218896851fd941c87c7a34b80075f9938e2
Binary files /dev/null and b/app/assets/images/emoji/grapes.png differ
diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fd51bd3915cdea8438b2fd5371ba37784ac8caf
Binary files /dev/null and b/app/assets/images/emoji/green_apple.png differ
diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5e411cf3b55f83dcdc4a33f3f86e84b952a986e
Binary files /dev/null and b/app/assets/images/emoji/green_book.png differ
diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..c52d60a58be84d498f02e7468f65e92a355dc758
Binary files /dev/null and b/app/assets/images/emoji/green_heart.png differ
diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b64da8bf7fc7d469eb997b4819446bd65862cce
Binary files /dev/null and b/app/assets/images/emoji/grey_exclamation.png differ
diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e7824c75f6c5b1e8cc0b118dae56e1f8ba70181
Binary files /dev/null and b/app/assets/images/emoji/grey_question.png differ
diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png
new file mode 100644
index 0000000000000000000000000000000000000000..871b2f071c9ef84368e2aebd8697bbc47c1f7902
Binary files /dev/null and b/app/assets/images/emoji/grimacing.png differ
diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png
new file mode 100644
index 0000000000000000000000000000000000000000..418d94c811b8b05722d2964fdf4deb5404952cec
Binary files /dev/null and b/app/assets/images/emoji/grin.png differ
diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e8e0dab78cd6ed0f6de60d9ebda0d63ff030391
Binary files /dev/null and b/app/assets/images/emoji/grinning.png differ
diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d7ab3c473cef67c8ce790ef58dffd62c470cc19
Binary files /dev/null and b/app/assets/images/emoji/guardsman.png differ
diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..cea9ba27468b662315b5671dcd444ee0631d8eab
Binary files /dev/null and b/app/assets/images/emoji/guardsman_tone1.png differ
diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..037464e4028e96301a7ae166c6c2913a7a10f5ad
Binary files /dev/null and b/app/assets/images/emoji/guardsman_tone2.png differ
diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f6726fbe878baff992d6ad3d9bcf1fc6084a145
Binary files /dev/null and b/app/assets/images/emoji/guardsman_tone3.png differ
diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..85fcf9a3b9764b2e2eeace77405962fca3ef38e2
Binary files /dev/null and b/app/assets/images/emoji/guardsman_tone4.png differ
diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5f9ca7d5a29c9d42c220262f22cbaa187a18615
Binary files /dev/null and b/app/assets/images/emoji/guardsman_tone5.png differ
diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png
new file mode 100644
index 0000000000000000000000000000000000000000..43d752f1e3d32522c38944e45d36e65738701325
Binary files /dev/null and b/app/assets/images/emoji/guitar.png differ
diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png
new file mode 100644
index 0000000000000000000000000000000000000000..89c5c244c7b5f908897cd199e791ceff1392a231
Binary files /dev/null and b/app/assets/images/emoji/gun.png differ
diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png
new file mode 100644
index 0000000000000000000000000000000000000000..91266b129308cc946500a2554897960e2a8a74c1
Binary files /dev/null and b/app/assets/images/emoji/haircut.png differ
diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c743b74abeb1bfb86cfb4644a4c50dd0ab548431
Binary files /dev/null and b/app/assets/images/emoji/haircut_tone1.png differ
diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f144f8e55ceb122967b69350e58c4b8a43844136
Binary files /dev/null and b/app/assets/images/emoji/haircut_tone2.png differ
diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d5ad19563acd74d812edf96302a9a8eb282563b8
Binary files /dev/null and b/app/assets/images/emoji/haircut_tone3.png differ
diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..244fd3af008d936439c4fbe0f590b71adee1d591
Binary files /dev/null and b/app/assets/images/emoji/haircut_tone4.png differ
diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..20a94a88623b3e2fc203d335994aba1a62ee3eba
Binary files /dev/null and b/app/assets/images/emoji/haircut_tone5.png differ
diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png
new file mode 100644
index 0000000000000000000000000000000000000000..3573b28a1fd516b60a7979e0896c9598531a0dcf
Binary files /dev/null and b/app/assets/images/emoji/hamburger.png differ
diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png
new file mode 100644
index 0000000000000000000000000000000000000000..00736cce47dda6084f5af070bfba0a1028591fba
Binary files /dev/null and b/app/assets/images/emoji/hammer.png differ
diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png
new file mode 100644
index 0000000000000000000000000000000000000000..3bee30ec588cecbaa8e17bdb10593da2e1cf3aa1
Binary files /dev/null and b/app/assets/images/emoji/hammer_pick.png differ
diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a04388e4e74b4ec554f4f3ca5c043bc0257ced0
Binary files /dev/null and b/app/assets/images/emoji/hamster.png differ
diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb5ae8ebb5accb50b04f34faa9172254bf087d1d
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed.png differ
diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7888e6bd23a3d16fbd33a0b9a07b03f12a599d5
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed_tone1.png differ
diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc10fbc272d84cea4960cca17d9a7ba9886a2216
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed_tone2.png differ
diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..707236ae8a4c64aafca2f147be9a376710eadf14
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed_tone3.png differ
diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..1430df9c61f21d089cd167cae619a72445d443e3
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed_tone4.png differ
diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..80bec971b6b75616fc7bc19ffc6c7b7f4d6f1051
Binary files /dev/null and b/app/assets/images/emoji/hand_splayed_tone5.png differ
diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png
new file mode 100644
index 0000000000000000000000000000000000000000..cbf75c5d25e7e620037960e89e95751783a54132
Binary files /dev/null and b/app/assets/images/emoji/handbag.png differ
diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png
new file mode 100644
index 0000000000000000000000000000000000000000..1152f1344c7a30bfa71b2edd9a781f3b803500ad
Binary files /dev/null and b/app/assets/images/emoji/handball.png differ
diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c26cac2df9860f913d011af2af97e3a3572d25a2
Binary files /dev/null and b/app/assets/images/emoji/handball_tone1.png differ
diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..7baaf95a9a2196a0525c3ac673356a5f92d8878b
Binary files /dev/null and b/app/assets/images/emoji/handball_tone2.png differ
diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e3a37c3d40845a0816cdfd30d7e4b79044aea9d
Binary files /dev/null and b/app/assets/images/emoji/handball_tone3.png differ
diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1233f38266a0d1de782717a69ef3e4373ae9260
Binary files /dev/null and b/app/assets/images/emoji/handball_tone4.png differ
diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b1eb9b64b0f791558066d3c2a3578e20e7e824c
Binary files /dev/null and b/app/assets/images/emoji/handball_tone5.png differ
diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5d35fd813830f338bd0b0789ba5d1b6a9948b8f
Binary files /dev/null and b/app/assets/images/emoji/handshake.png differ
diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f8fbb9bdcae0be2f002bf75853dfb2554f872ab
Binary files /dev/null and b/app/assets/images/emoji/handshake_tone1.png differ
diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..336a77a6d78122d2e39467cb264ec3cb61be9476
Binary files /dev/null and b/app/assets/images/emoji/handshake_tone2.png differ
diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..95f62d4fecd311564f53237f292a11b16e86da7e
Binary files /dev/null and b/app/assets/images/emoji/handshake_tone3.png differ
diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b0a6433886487d1f24849cf02e6b0c190d8af76
Binary files /dev/null and b/app/assets/images/emoji/handshake_tone4.png differ
diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..40189ee68e4f6853f0ee65818f383ec009c5d8e4
Binary files /dev/null and b/app/assets/images/emoji/handshake_tone5.png differ
diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e26f0070b08c75c5b518f50d515b18bd56a704f
Binary files /dev/null and b/app/assets/images/emoji/hash.png differ
diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png
new file mode 100644
index 0000000000000000000000000000000000000000..31dfb511e0ed2f0cba50e1cdec695efcd7f1c9e7
Binary files /dev/null and b/app/assets/images/emoji/hatched_chick.png differ
diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5b0e8f3bcccf44c25b6d5c59c8838bde15f7a52
Binary files /dev/null and b/app/assets/images/emoji/hatching_chick.png differ
diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png
new file mode 100644
index 0000000000000000000000000000000000000000..0be723085e0141cebe2803c891d805903876c6ca
Binary files /dev/null and b/app/assets/images/emoji/head_bandage.png differ
diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9fd34041d87c64edfc312c680fc5fd03f54605d
Binary files /dev/null and b/app/assets/images/emoji/headphones.png differ
diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png
new file mode 100644
index 0000000000000000000000000000000000000000..74b6be0c6c54ac442b3eb742f4790ff33955c543
Binary files /dev/null and b/app/assets/images/emoji/hear_no_evil.png differ
diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..638cb72dc4e6d09f22662e6e607516f4310d3f0c
Binary files /dev/null and b/app/assets/images/emoji/heart.png differ
diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png
new file mode 100644
index 0000000000000000000000000000000000000000..5443f60bc6389dfddd6728cebae38a046f0efb00
Binary files /dev/null and b/app/assets/images/emoji/heart_decoration.png differ
diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png
new file mode 100644
index 0000000000000000000000000000000000000000..91b520be40b5f166b237a234de78624dc8ad9aa8
Binary files /dev/null and b/app/assets/images/emoji/heart_exclamation.png differ
diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..73fbee29d4eef6c12fd2175873f9a1b2333841e7
Binary files /dev/null and b/app/assets/images/emoji/heart_eyes.png differ
diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc5a833f9a1a89161bb45a167f3a9e9cb1cff478
Binary files /dev/null and b/app/assets/images/emoji/heart_eyes_cat.png differ
diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png
new file mode 100644
index 0000000000000000000000000000000000000000..0bcf2d1d567053ef7480cb2b3630828a422729a3
Binary files /dev/null and b/app/assets/images/emoji/heartbeat.png differ
diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6e694e972f124f3fe481ebbe0458b7b3a698000
Binary files /dev/null and b/app/assets/images/emoji/heartpulse.png differ
diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png
new file mode 100644
index 0000000000000000000000000000000000000000..393c3ed52674a2856b2a6993fc63ca41567a3ce6
Binary files /dev/null and b/app/assets/images/emoji/hearts.png differ
diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..03bd695377e9592ff6276239c2413c79b9afe364
Binary files /dev/null and b/app/assets/images/emoji/heavy_check_mark.png differ
diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..df32ab21bea3c7e837af401763208d14a1944b2d
Binary files /dev/null and b/app/assets/images/emoji/heavy_division_sign.png differ
diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef2c2e205902b66f2195e04843611560b09d0c65
Binary files /dev/null and b/app/assets/images/emoji/heavy_dollar_sign.png differ
diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..054211caf1224c77f4e56824a2f461f27d2e058c
Binary files /dev/null and b/app/assets/images/emoji/heavy_minus_sign.png differ
diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png
new file mode 100644
index 0000000000000000000000000000000000000000..e47cc1b685d167f150075da93668f322d320ed2c
Binary files /dev/null and b/app/assets/images/emoji/heavy_multiplication_x.png differ
diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..40799798aafeb50aa74f8362dbae65330a45fc45
Binary files /dev/null and b/app/assets/images/emoji/heavy_plus_sign.png differ
diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ec5f39a51a220dbdffb4328c892e52b1c0a9bf8
Binary files /dev/null and b/app/assets/images/emoji/helicopter.png differ
diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png
new file mode 100644
index 0000000000000000000000000000000000000000..7140a6760386565c6b5299265f23aa01e5efb925
Binary files /dev/null and b/app/assets/images/emoji/helmet_with_cross.png differ
diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png
new file mode 100644
index 0000000000000000000000000000000000000000..d984d1562bb40168a7daf473d1b97e7848347ca8
Binary files /dev/null and b/app/assets/images/emoji/herb.png differ
diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png
new file mode 100644
index 0000000000000000000000000000000000000000..39dd3524233b5b974b4402161566a874fd4b6c04
Binary files /dev/null and b/app/assets/images/emoji/hibiscus.png differ
diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png
new file mode 100644
index 0000000000000000000000000000000000000000..c41f2d5fd5031950f02fceb5f94956190355c224
Binary files /dev/null and b/app/assets/images/emoji/high_brightness.png differ
diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png
new file mode 100644
index 0000000000000000000000000000000000000000..b331cbccc9df91d13d72624364c87f5157033a96
Binary files /dev/null and b/app/assets/images/emoji/high_heel.png differ
diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png
new file mode 100644
index 0000000000000000000000000000000000000000..be94e9cbf73ee6ed49656ae3de9a355d065b3d2f
Binary files /dev/null and b/app/assets/images/emoji/hockey.png differ
diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png
new file mode 100644
index 0000000000000000000000000000000000000000..517d2ae0debf6c0808cd45b12fae68ddeea742cd
Binary files /dev/null and b/app/assets/images/emoji/hole.png differ
diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ab4a2a26514697070febcb63e5631c35227d7d5
Binary files /dev/null and b/app/assets/images/emoji/homes.png differ
diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d8f592955e82b9d431fa290ab43a8d573be9760
Binary files /dev/null and b/app/assets/images/emoji/honey_pot.png differ
diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cb1172f4e485067cc829510b8c096edcffe69e4
Binary files /dev/null and b/app/assets/images/emoji/horse.png differ
diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png
new file mode 100644
index 0000000000000000000000000000000000000000..addf9edac56f56d1f93c57aabdf972b22bbae31d
Binary files /dev/null and b/app/assets/images/emoji/horse_racing.png differ
diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9bf4092e98500bff502408b0f795556f299efc6
Binary files /dev/null and b/app/assets/images/emoji/horse_racing_tone1.png differ
diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..031bbc3d867d94ceebefebf0e1573a2da31df56d
Binary files /dev/null and b/app/assets/images/emoji/horse_racing_tone2.png differ
diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b40ef891f9bbc4ba4d1db5d0141ba114558bca67
Binary files /dev/null and b/app/assets/images/emoji/horse_racing_tone3.png differ
diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e286cb85065cb34a0b9d32c3dbb049d3e92a2fa0
Binary files /dev/null and b/app/assets/images/emoji/horse_racing_tone4.png differ
diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..453c51c6007d448a5ef3bded2b140395ca8e9f34
Binary files /dev/null and b/app/assets/images/emoji/horse_racing_tone5.png differ
diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png
new file mode 100644
index 0000000000000000000000000000000000000000..1cbce4ae76705364a64b16edc3dcccfbc2c8b570
Binary files /dev/null and b/app/assets/images/emoji/hospital.png differ
diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png
new file mode 100644
index 0000000000000000000000000000000000000000..266675bd5774d124166bdf6cb2937ad2947d5f5a
Binary files /dev/null and b/app/assets/images/emoji/hot_pepper.png differ
diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c3354d94cbd739cc8dd71ff93c48f5f6e0ffa66
Binary files /dev/null and b/app/assets/images/emoji/hotdog.png differ
diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea8f4c4979aebc36a5137a8fefcc99fec103d80e
Binary files /dev/null and b/app/assets/images/emoji/hotel.png differ
diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d9df2d9475818ba09efb38c4f9d0323ea599593
Binary files /dev/null and b/app/assets/images/emoji/hotsprings.png differ
diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5db2d1d3f4b504b8be4fd3f70cf07348e489374
Binary files /dev/null and b/app/assets/images/emoji/hourglass.png differ
diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png
new file mode 100644
index 0000000000000000000000000000000000000000..b93b15ed6d8532055f3b5f1c42b46219fd272d4c
Binary files /dev/null and b/app/assets/images/emoji/hourglass_flowing_sand.png differ
diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png
new file mode 100644
index 0000000000000000000000000000000000000000..01c98a0ba923561809a25825d2722ebfe0a33467
Binary files /dev/null and b/app/assets/images/emoji/house.png differ
diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png
new file mode 100644
index 0000000000000000000000000000000000000000..c55e81de990e8780bcd60d9fdabe0b524460b764
Binary files /dev/null and b/app/assets/images/emoji/house_abandoned.png differ
diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png
new file mode 100644
index 0000000000000000000000000000000000000000..0aae41598ef728d913a8feea8b65619058c1e1d8
Binary files /dev/null and b/app/assets/images/emoji/house_with_garden.png differ
diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bba6dc6d51ba8149a3b8d87c11ab048addd4bbb
Binary files /dev/null and b/app/assets/images/emoji/hugging.png differ
diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png
new file mode 100644
index 0000000000000000000000000000000000000000..cad0e23132ebeabdb118073435654f6391eba2ee
Binary files /dev/null and b/app/assets/images/emoji/hushed.png differ
diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png
new file mode 100644
index 0000000000000000000000000000000000000000..94267b9c43460ce7ffda6ae85dcb96d0b1a78e2c
Binary files /dev/null and b/app/assets/images/emoji/ice_cream.png differ
diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c449b0c03987516a03610fe1195751a884abc39
Binary files /dev/null and b/app/assets/images/emoji/ice_skate.png differ
diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f6546e31a536c8bb0877b4d782ba8b23a5de5c0
Binary files /dev/null and b/app/assets/images/emoji/icecream.png differ
diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bf69bf7ba8b661659149b98ab3d05362dda2842
Binary files /dev/null and b/app/assets/images/emoji/id.png differ
diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c0d589caf05fb992c3a24271653616c6faf2b37
Binary files /dev/null and b/app/assets/images/emoji/ideograph_advantage.png differ
diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f9a96055394c950ab5db1765d1defba3447fcc3
Binary files /dev/null and b/app/assets/images/emoji/imp.png differ
diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png
new file mode 100644
index 0000000000000000000000000000000000000000..41a6be2b0ee48754956920d00a60a8e82d3ec089
Binary files /dev/null and b/app/assets/images/emoji/inbox_tray.png differ
diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd22e88182e6b8e38dae8ccc76c450b2b47a89ea
Binary files /dev/null and b/app/assets/images/emoji/incoming_envelope.png differ
diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png
new file mode 100644
index 0000000000000000000000000000000000000000..55fc6294d25bfd2f768637467ae3cd685ba69c63
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person.png differ
diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d9e224794056f176e16e11feda0a1a08d0bda16
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person_tone1.png differ
diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..879e8b7966d791358bcdeea1af678f6c1c037905
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person_tone2.png differ
diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..307514eab67971a2c1433ff6337df5329bd4d591
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person_tone3.png differ
diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..297395dcb3f8b9afa8a9e34c75d6976362e4387e
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person_tone4.png differ
diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..26f8f22b28be8f32d755e702573ba5e8eea51cb5
Binary files /dev/null and b/app/assets/images/emoji/information_desk_person_tone5.png differ
diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png
new file mode 100644
index 0000000000000000000000000000000000000000..871f2db931416519a18cfc92439e53fa92f179c6
Binary files /dev/null and b/app/assets/images/emoji/information_source.png differ
diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png
new file mode 100644
index 0000000000000000000000000000000000000000..57f5151124fbb01644beda0259b718ef61cde2f6
Binary files /dev/null and b/app/assets/images/emoji/innocent.png differ
diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png
new file mode 100644
index 0000000000000000000000000000000000000000..509813e9bb225eb88fea2dd9100d6d6293c526a4
Binary files /dev/null and b/app/assets/images/emoji/interrobang.png differ
diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd377acf872034e8e29ae7dd2bd54d81b6a825d4
Binary files /dev/null and b/app/assets/images/emoji/iphone.png differ
diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png
new file mode 100644
index 0000000000000000000000000000000000000000..7fd834389b7dfdfafd5aad19777fc40120742ec5
Binary files /dev/null and b/app/assets/images/emoji/island.png differ
diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png
new file mode 100644
index 0000000000000000000000000000000000000000..dfd933f6f3609abdeca0d492da99d1b0f105257d
Binary files /dev/null and b/app/assets/images/emoji/izakaya_lantern.png differ
diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png
new file mode 100644
index 0000000000000000000000000000000000000000..44c3fc0aec9a51ccd6196793be786a8e411c72da
Binary files /dev/null and b/app/assets/images/emoji/jack_o_lantern.png differ
diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png
new file mode 100644
index 0000000000000000000000000000000000000000..d86d0a59e1226ca5647971e8e598a7021f45773e
Binary files /dev/null and b/app/assets/images/emoji/japan.png differ
diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png
new file mode 100644
index 0000000000000000000000000000000000000000..64b4e33a1ae7b21c4d307be15b9cbcdbf0868154
Binary files /dev/null and b/app/assets/images/emoji/japanese_castle.png differ
diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png
new file mode 100644
index 0000000000000000000000000000000000000000..515c6a2250e19903c4d940158a04beb2f8924dcb
Binary files /dev/null and b/app/assets/images/emoji/japanese_goblin.png differ
diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe8670fdaf1d7a98ee705ce530fb1af54a8475d7
Binary files /dev/null and b/app/assets/images/emoji/japanese_ogre.png differ
diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a6869d674c57f9fb64ae13ba1269df7f43b3a73
Binary files /dev/null and b/app/assets/images/emoji/jeans.png differ
diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ba3b1859d82a27fed4f3dd2e865fc271582d347
Binary files /dev/null and b/app/assets/images/emoji/joy.png differ
diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..aac353179aa84a9951144e2bf55a73d8c67b98ef
Binary files /dev/null and b/app/assets/images/emoji/joy_cat.png differ
diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ee1905434e84fc941523abecf09279681bf5d1e
Binary files /dev/null and b/app/assets/images/emoji/joystick.png differ
diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png
new file mode 100644
index 0000000000000000000000000000000000000000..a37f6224a42eebd834ba4bab9da5fbe331a29380
Binary files /dev/null and b/app/assets/images/emoji/juggling.png differ
diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c18eda400311a4a925855f4852de9b4ed80a2c24
Binary files /dev/null and b/app/assets/images/emoji/juggling_tone1.png differ
diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..de3b7a555b6cab0533b747ec08fedced0232427e
Binary files /dev/null and b/app/assets/images/emoji/juggling_tone2.png differ
diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..74ab6d8545881acbef88245a6cf1783a9c390240
Binary files /dev/null and b/app/assets/images/emoji/juggling_tone3.png differ
diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c57823203fcabb3ccb2bfb6b79eaadeb9a8cdcf
Binary files /dev/null and b/app/assets/images/emoji/juggling_tone4.png differ
diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c343d6ee98a9135e8f6233384cbe381583ca60e4
Binary files /dev/null and b/app/assets/images/emoji/juggling_tone5.png differ
diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png
new file mode 100644
index 0000000000000000000000000000000000000000..1778c1138e4c4780be595c2563b7622524335eac
Binary files /dev/null and b/app/assets/images/emoji/kaaba.png differ
diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png
new file mode 100644
index 0000000000000000000000000000000000000000..319cd1b884cfa844fbfc76f3d220cc4bb8474798
Binary files /dev/null and b/app/assets/images/emoji/key.png differ
diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png
new file mode 100644
index 0000000000000000000000000000000000000000..e11d706c6c8e79b99f285637acf97afe3d56369f
Binary files /dev/null and b/app/assets/images/emoji/key2.png differ
diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..75027cb9af7eda27edaa64e673873bdbe87e4213
Binary files /dev/null and b/app/assets/images/emoji/keyboard.png differ
diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png
new file mode 100644
index 0000000000000000000000000000000000000000..abe851115d1a4776ab82c5cb60c1c1a49b09c0ae
Binary files /dev/null and b/app/assets/images/emoji/kimono.png differ
diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png
new file mode 100644
index 0000000000000000000000000000000000000000..85e6dcfc4e8d980bf57d9139f51f1e058025336f
Binary files /dev/null and b/app/assets/images/emoji/kiss.png differ
diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9a0edae17c641c4c5e0a04b88514893e6db3865
Binary files /dev/null and b/app/assets/images/emoji/kiss_mm.png differ
diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdac73cbb1daaabaa7c71f048f8a5ccf8affca59
Binary files /dev/null and b/app/assets/images/emoji/kiss_ww.png differ
diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png
new file mode 100644
index 0000000000000000000000000000000000000000..39d325fd8e3ec36d67465df9bc06dc75665ec544
Binary files /dev/null and b/app/assets/images/emoji/kissing.png differ
diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e0bcc77540eb635c24878c7d1b631c32deeba9a
Binary files /dev/null and b/app/assets/images/emoji/kissing_cat.png differ
diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..b684d7d4d6c0615dee4b885fd0ff65d46ca93020
Binary files /dev/null and b/app/assets/images/emoji/kissing_closed_eyes.png differ
diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ff808fd6143aee0600afb530ab38f50b74c9951
Binary files /dev/null and b/app/assets/images/emoji/kissing_heart.png differ
diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..e181f17099d4c449d474f950da03996d909fcaa2
Binary files /dev/null and b/app/assets/images/emoji/kissing_smiling_eyes.png differ
diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png
new file mode 100644
index 0000000000000000000000000000000000000000..dfbd825807411dc76fe93fcc476be44e2e4681b6
Binary files /dev/null and b/app/assets/images/emoji/kiwi.png differ
diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png
new file mode 100644
index 0000000000000000000000000000000000000000..1acb9f3077b3268056b807124fb284ede087fc28
Binary files /dev/null and b/app/assets/images/emoji/knife.png differ
diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0aa437a98c18bf1fa951db1967a59a65884d79b
Binary files /dev/null and b/app/assets/images/emoji/koala.png differ
diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png
new file mode 100644
index 0000000000000000000000000000000000000000..6450eb44d90c3ea67741ffe85d566376d9072473
Binary files /dev/null and b/app/assets/images/emoji/koko.png differ
diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png
new file mode 100644
index 0000000000000000000000000000000000000000..d41c9b4f1e1f9617f609f604ea1f5bf44e6a08b7
Binary files /dev/null and b/app/assets/images/emoji/label.png differ
diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png
new file mode 100644
index 0000000000000000000000000000000000000000..84078ef3127169d9bcf95e52283ef55606d0a79c
Binary files /dev/null and b/app/assets/images/emoji/large_blue_circle.png differ
diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png
new file mode 100644
index 0000000000000000000000000000000000000000..416a58bd5a8945f5ed83d66de6db600180029060
Binary files /dev/null and b/app/assets/images/emoji/large_blue_diamond.png differ
diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png
new file mode 100644
index 0000000000000000000000000000000000000000..73ff0ac36c800c41bad62347d6323a31f96b08f5
Binary files /dev/null and b/app/assets/images/emoji/large_orange_diamond.png differ
diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0842a0dd4081280377dcd75d155dc230384c39af
Binary files /dev/null and b/app/assets/images/emoji/last_quarter_moon.png differ
diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..94099343c5d85b91a5c815c44c31c3f7ac26ac9b
Binary files /dev/null and b/app/assets/images/emoji/last_quarter_moon_with_face.png differ
diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png
new file mode 100644
index 0000000000000000000000000000000000000000..d94e9505ba176f74e3e16640a429eb14953eb3a0
Binary files /dev/null and b/app/assets/images/emoji/laughing.png differ
diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e43e1af820b3f8ebb0f29837ebef36430ceb4af
Binary files /dev/null and b/app/assets/images/emoji/leaves.png differ
diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png
new file mode 100644
index 0000000000000000000000000000000000000000..13e7561a4bd0d1780e1564961c963088f03af77e
Binary files /dev/null and b/app/assets/images/emoji/ledger.png differ
diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9d9fd8d59ce23431c14acb6341055bfb03501a2
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist.png differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1262a6b4b6938311c62b652737f81722d72425a5
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist_tone1.png differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..40bf70b82b295a6e9da468d96f907f768781e73a
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist_tone2.png differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..93f581451115fd62eeb4bfe1a2cee900b0e6df34
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist_tone3.png differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..d82b5ec91f0af48d924de5509dd1fad30855efef
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist_tone4.png differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..09ae4cd492bd908143387dc057909012adc49ac3
Binary files /dev/null and b/app/assets/images/emoji/left_facing_fist_tone5.png differ
diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png
new file mode 100644
index 0000000000000000000000000000000000000000..887b23f3f25b5423cf817db4c40725b3f2fe9d02
Binary files /dev/null and b/app/assets/images/emoji/left_luggage.png differ
diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..7937f24f2acd15d4452637ba72abd9f6a03c184e
Binary files /dev/null and b/app/assets/images/emoji/left_right_arrow.png differ
diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba45c2ad9e9cfa900bcb93fdc54beb9f26ea6fcf
Binary files /dev/null and b/app/assets/images/emoji/leftwards_arrow_with_hook.png differ
diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a7d95ca2209b8b438f6fbc15b34695c0a7bb620
Binary files /dev/null and b/app/assets/images/emoji/lemon.png differ
diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30158d34de9147dd1967a09d801b327f9225e743
Binary files /dev/null and b/app/assets/images/emoji/leo.png differ
diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aac3d4944810cf9bde20a8f96e70ecf1842ea5e
Binary files /dev/null and b/app/assets/images/emoji/leopard.png differ
diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png
new file mode 100644
index 0000000000000000000000000000000000000000..720a3b34119ef6e06971b42b07e6cac8102675a9
Binary files /dev/null and b/app/assets/images/emoji/level_slider.png differ
diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png
new file mode 100644
index 0000000000000000000000000000000000000000..3dc315a3d91f06c2fc5048be500c1eb9277f7a6c
Binary files /dev/null and b/app/assets/images/emoji/levitate.png differ
diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fd133a357c71681d37ea6492b13452e6ebd9465
Binary files /dev/null and b/app/assets/images/emoji/libra.png differ
diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png
new file mode 100644
index 0000000000000000000000000000000000000000..afdeaa476af88d3d5b274e7fac193d74e1d85536
Binary files /dev/null and b/app/assets/images/emoji/lifter.png differ
diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..febaad123ece634a5b22246fc9fd8a8c0fbb97e9
Binary files /dev/null and b/app/assets/images/emoji/lifter_tone1.png differ
diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..27ae794a18e882a2e883749e8472eb9993301485
Binary files /dev/null and b/app/assets/images/emoji/lifter_tone2.png differ
diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..45c4c22c7098daee356dd17666fb9884af2f5e38
Binary files /dev/null and b/app/assets/images/emoji/lifter_tone3.png differ
diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..67dd21d2464e2d42b825986b923f375df8d5da19
Binary files /dev/null and b/app/assets/images/emoji/lifter_tone4.png differ
diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa0152038b6ca7ea495f9811d72ccfa0bc881462
Binary files /dev/null and b/app/assets/images/emoji/lifter_tone5.png differ
diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png
new file mode 100644
index 0000000000000000000000000000000000000000..a64829f5078f9bbb76c52ddb1c9a68505beada23
Binary files /dev/null and b/app/assets/images/emoji/light_rail.png differ
diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae20f0f8eecdfb6f64c9b176f1f9d9a9135be16a
Binary files /dev/null and b/app/assets/images/emoji/link.png differ
diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..5062ab47ecf8f395d0b6f44519fd46d91d70ed8e
Binary files /dev/null and b/app/assets/images/emoji/lion_face.png differ
diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png
new file mode 100644
index 0000000000000000000000000000000000000000..35f3cc2006f4ebdf96fb5666b320ac40c66d34d9
Binary files /dev/null and b/app/assets/images/emoji/lips.png differ
diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png
new file mode 100644
index 0000000000000000000000000000000000000000..61a0c084c99049193cc5cf18762b16f8acd6becd
Binary files /dev/null and b/app/assets/images/emoji/lipstick.png differ
diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png
new file mode 100644
index 0000000000000000000000000000000000000000..8363876050e2a11696474a06176a340f391f9a03
Binary files /dev/null and b/app/assets/images/emoji/lizard.png differ
diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a739c46644b47ee44456780e10cdd0e6ee019b9
Binary files /dev/null and b/app/assets/images/emoji/lock.png differ
diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png
new file mode 100644
index 0000000000000000000000000000000000000000..19a07d162fb4d41f51c2f5a6f148303463188cf5
Binary files /dev/null and b/app/assets/images/emoji/lock_with_ink_pen.png differ
diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad76d7bf916c7d26a29b6e8a2d8cfe3fdf388cdd
Binary files /dev/null and b/app/assets/images/emoji/lollipop.png differ
diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b82c8fe31555bb81051e706713c3a696f843069
Binary files /dev/null and b/app/assets/images/emoji/loop.png differ
diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png
new file mode 100644
index 0000000000000000000000000000000000000000..8370033a53995fb1a16c089c1cdf0ffd2b12b87f
Binary files /dev/null and b/app/assets/images/emoji/loud_sound.png differ
diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fd76a95b8203acbde661331ee3f79ac962ef210
Binary files /dev/null and b/app/assets/images/emoji/loudspeaker.png differ
diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e136be6f8b5b915c4cdba30f7392a9dc8759dd5
Binary files /dev/null and b/app/assets/images/emoji/love_hotel.png differ
diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c3c767e784b6cc35bf0d3e4581e7adb12f353ed
Binary files /dev/null and b/app/assets/images/emoji/love_letter.png differ
diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png
new file mode 100644
index 0000000000000000000000000000000000000000..543011d3961c5e083ba0ac092b241867c812171a
Binary files /dev/null and b/app/assets/images/emoji/low_brightness.png differ
diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..02827e2628b96077cecf75ee83965beed976ec54
Binary files /dev/null and b/app/assets/images/emoji/lying_face.png differ
diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png
new file mode 100644
index 0000000000000000000000000000000000000000..8a3506fc1d7fe311c35c7f2e331a0d9c2d3114b4
Binary files /dev/null and b/app/assets/images/emoji/m.png differ
diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png
new file mode 100644
index 0000000000000000000000000000000000000000..55487156ac62b38c712bde68397599022e6d6598
Binary files /dev/null and b/app/assets/images/emoji/mag.png differ
diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f4b1bca876d8d7a06e10bca13bfb88ccddad088
Binary files /dev/null and b/app/assets/images/emoji/mag_right.png differ
diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png
new file mode 100644
index 0000000000000000000000000000000000000000..66fd32025b22fc017ddc852f62660c55a92ba7a7
Binary files /dev/null and b/app/assets/images/emoji/mahjong.png differ
diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef5174e40dd8edf83777685df4ea7edfc61d5cbb
Binary files /dev/null and b/app/assets/images/emoji/mailbox.png differ
diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png
new file mode 100644
index 0000000000000000000000000000000000000000..ddc705db0d83c2a505de05be88d0bdfd8c38d36c
Binary files /dev/null and b/app/assets/images/emoji/mailbox_closed.png differ
diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png
new file mode 100644
index 0000000000000000000000000000000000000000..5460616a5b1dcaf14ebe3f04875ac3db79b64a7e
Binary files /dev/null and b/app/assets/images/emoji/mailbox_with_mail.png differ
diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9aeee6b15a7173ed73fbd91a3b455202a0e5265
Binary files /dev/null and b/app/assets/images/emoji/mailbox_with_no_mail.png differ
diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png
new file mode 100644
index 0000000000000000000000000000000000000000..857a02e5146248f961f78754049517a7c9b43eb4
Binary files /dev/null and b/app/assets/images/emoji/man.png differ
diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png
new file mode 100644
index 0000000000000000000000000000000000000000..ccff3bede5a0fdd88bd6d4b370c935104365438b
Binary files /dev/null and b/app/assets/images/emoji/man_dancing.png differ
diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0b9f82d90563ff73d5d51c7b74d239c470733b1
Binary files /dev/null and b/app/assets/images/emoji/man_dancing_tone1.png differ
diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5beed56e2e007438fbc0144a123e0aec90a6d54
Binary files /dev/null and b/app/assets/images/emoji/man_dancing_tone2.png differ
diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fa20180a6ee54dfdf7c9446cd2a5f905f6b2189
Binary files /dev/null and b/app/assets/images/emoji/man_dancing_tone3.png differ
diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd3528c83bab080d5f3d306cdb423c730e98db2c
Binary files /dev/null and b/app/assets/images/emoji/man_dancing_tone4.png differ
diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..41fd4f880c9cf6d9802fe425543561ff66ae05c7
Binary files /dev/null and b/app/assets/images/emoji/man_dancing_tone5.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f7e9303f893bee5e08392aeb2d7f88f328f280e
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b6b3acd99b510d5a0faaa91e2ab633173905eb6
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo_tone1.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..7975191b36047c6da937ffed1213eb11eb9937e5
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo_tone2.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2816f600ae7996bf45fd06ef9b6f8f41356ed5a
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo_tone3.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea8291760f9329df7861f63ce6aa1d8af5677ced
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo_tone4.png differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c743e05fc5e135a7d52fb76faed8b3c666a8378a
Binary files /dev/null and b/app/assets/images/emoji/man_in_tuxedo_tone5.png differ
diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb86e963a800b6a81db3079ceb34cff08e59580d
Binary files /dev/null and b/app/assets/images/emoji/man_tone1.png differ
diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdeeaff46f51acea57159640ed12a62b69af64e3
Binary files /dev/null and b/app/assets/images/emoji/man_tone2.png differ
diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ae0b5df9cff7feebb4553a60a260764a6d39fca
Binary files /dev/null and b/app/assets/images/emoji/man_tone3.png differ
diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..db14cde99b813b3c7307fdd70b60ef4727542d1b
Binary files /dev/null and b/app/assets/images/emoji/man_tone4.png differ
diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c67a70529c45e78c5bca7cb5230b50b1fcc19d0
Binary files /dev/null and b/app/assets/images/emoji/man_tone5.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png
new file mode 100644
index 0000000000000000000000000000000000000000..7841e13608df335878939b0b8a86bd86136f2fc0
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b7b3def19c60dacbc78412052996a5d28d7eee6
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8b9cf87f4bb6e57cdfb6d263e274d1ed5b3530b
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..effdd0c4c84c9800e2ae9a208f1d6e2cfd956b58
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..f885ff46fa18e26dae2d328a6e0868a4d88a1036
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6d55ca1380b0ead1d9c9becf2cdebf4c2d180d0
Binary files /dev/null and b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png differ
diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png
new file mode 100644
index 0000000000000000000000000000000000000000..51cf047f966637679099f2e04f13aa600b48ca89
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban.png differ
diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e12ee4b231d243981dabab62cb4d4b2a31fddb2
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban_tone1.png differ
diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..37de4cceb23a43389f153e2e98786fd5057204e0
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban_tone2.png differ
diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..f607afd3450750e47e8c74cc93708e1b34b55e13
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban_tone3.png differ
diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..c05695888af1ada4fa649384ffa2c0223626f550
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban_tone4.png differ
diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b4ff64720b50d31bfc2c4c980c54defdda6ba32
Binary files /dev/null and b/app/assets/images/emoji/man_with_turban_tone5.png differ
diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bf7541032c6a7e5da87f253eeecd6ead35f9140
Binary files /dev/null and b/app/assets/images/emoji/mans_shoe.png differ
diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png
new file mode 100644
index 0000000000000000000000000000000000000000..15efe32c7987f4828aa89ce20cf2134364379264
Binary files /dev/null and b/app/assets/images/emoji/map.png differ
diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png
new file mode 100644
index 0000000000000000000000000000000000000000..c49acea67f741de89e6d31e6d63b4fe4dfadc633
Binary files /dev/null and b/app/assets/images/emoji/maple_leaf.png differ
diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d6114761f65c4939f4b79d30ec584b8c9212ff9
Binary files /dev/null and b/app/assets/images/emoji/martial_arts_uniform.png differ
diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e800acd1c06b183937d52f210431486006f32de
Binary files /dev/null and b/app/assets/images/emoji/mask.png differ
diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png
new file mode 100644
index 0000000000000000000000000000000000000000..b91d845e3741ef09ed55a3d1bfbd696eed049334
Binary files /dev/null and b/app/assets/images/emoji/massage.png differ
diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0f415d3186310a456feff4c2ae8f477cffbf2c3
Binary files /dev/null and b/app/assets/images/emoji/massage_tone1.png differ
diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0bb244a270b3c7cd6815526d87ba1de0fc409ba6
Binary files /dev/null and b/app/assets/images/emoji/massage_tone2.png differ
diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a117ee81a222669f6e1d43db990bc21674d544e3
Binary files /dev/null and b/app/assets/images/emoji/massage_tone3.png differ
diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f42ab017f4e4b1d58d14a3ab97f5eda73414973
Binary files /dev/null and b/app/assets/images/emoji/massage_tone4.png differ
diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a388c0d0b5f361567f03c57c10c78810f3c47f0
Binary files /dev/null and b/app/assets/images/emoji/massage_tone5.png differ
diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png
new file mode 100644
index 0000000000000000000000000000000000000000..b20a59d1690285f118abc4b9540612b2a28142c0
Binary files /dev/null and b/app/assets/images/emoji/meat_on_bone.png differ
diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png
new file mode 100644
index 0000000000000000000000000000000000000000..b85896b14da28a425120889d71eb8b5c2cb45eb5
Binary files /dev/null and b/app/assets/images/emoji/medal.png differ
diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e6735188e382184c96bed35b19db0def7b84589
Binary files /dev/null and b/app/assets/images/emoji/mega.png differ
diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png
new file mode 100644
index 0000000000000000000000000000000000000000..c01232d419de890de0d3a8b2b22d597c0c968983
Binary files /dev/null and b/app/assets/images/emoji/melon.png differ
diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4297362869849740c0f90f383980fb317714699
Binary files /dev/null and b/app/assets/images/emoji/menorah.png differ
diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5a1e1ba0cd4980d55925a8698f1f52ef8d34b66
Binary files /dev/null and b/app/assets/images/emoji/mens.png differ
diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png
new file mode 100644
index 0000000000000000000000000000000000000000..4aa6e7e0a44a18d0626d1e1f0ebffb101d088df1
Binary files /dev/null and b/app/assets/images/emoji/metal.png differ
diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c080d2addbda36e8a3923d126989f037052bf711
Binary files /dev/null and b/app/assets/images/emoji/metal_tone1.png differ
diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..12313529bcf01ad79855678314901e1493c14dc6
Binary files /dev/null and b/app/assets/images/emoji/metal_tone2.png differ
diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca9be6ae67b8a95e458cde2e60701c43b63d180b
Binary files /dev/null and b/app/assets/images/emoji/metal_tone3.png differ
diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..abe28cbf890a768b8522ebd080b4a5c38b7bcb90
Binary files /dev/null and b/app/assets/images/emoji/metal_tone4.png differ
diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c6b5dd34ed5f977c76355dfc44fb0cf6adeb66d
Binary files /dev/null and b/app/assets/images/emoji/metal_tone5.png differ
diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png
new file mode 100644
index 0000000000000000000000000000000000000000..1de8f0551f3b89a504e1e20495da2f90c73ced87
Binary files /dev/null and b/app/assets/images/emoji/metro.png differ
diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4e6b0def25c26b551a2101f0ac0a669d42fe3fb
Binary files /dev/null and b/app/assets/images/emoji/microphone.png differ
diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd9167654ffb69fc7cf653d2f34b95cb6107119a
Binary files /dev/null and b/app/assets/images/emoji/microphone2.png differ
diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png
new file mode 100644
index 0000000000000000000000000000000000000000..90f5acf6a78277a6532db9d3d10089b06bd5d63f
Binary files /dev/null and b/app/assets/images/emoji/microscope.png differ
diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png
new file mode 100644
index 0000000000000000000000000000000000000000..697f7a25eb219fb269bdd4749284e6835958a186
Binary files /dev/null and b/app/assets/images/emoji/middle_finger.png differ
diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..61ef12a154845d363ed50458272a1772933d0683
Binary files /dev/null and b/app/assets/images/emoji/middle_finger_tone1.png differ
diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c31a69be9af9ea608294f812ccaf1718d31088d3
Binary files /dev/null and b/app/assets/images/emoji/middle_finger_tone2.png differ
diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..73ac216ce63c1de027daf405653be853f0cdeda7
Binary files /dev/null and b/app/assets/images/emoji/middle_finger_tone3.png differ
diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..80b8ab7706d42800729571f27ff380a095d55d1d
Binary files /dev/null and b/app/assets/images/emoji/middle_finger_tone4.png differ
diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8826b196e881877b981ea7822a49a8c1350592e
Binary files /dev/null and b/app/assets/images/emoji/middle_finger_tone5.png differ
diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png
new file mode 100644
index 0000000000000000000000000000000000000000..ecd3fb0358453173ce77174b8068d5a2e4ae6fa2
Binary files /dev/null and b/app/assets/images/emoji/military_medal.png differ
diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4fcf2e64f39a91fc84ddbcf6e7bfe6ce4da5f3b
Binary files /dev/null and b/app/assets/images/emoji/milk.png differ
diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2b8ac59c5e9107bf393b64e92dea3ca0a344c1c
Binary files /dev/null and b/app/assets/images/emoji/milky_way.png differ
diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png
new file mode 100644
index 0000000000000000000000000000000000000000..c60dd8f47ab082a34b826ddac0de4ea317a18730
Binary files /dev/null and b/app/assets/images/emoji/minibus.png differ
diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fa94cfbe746be435f937865c143ab6471ef8278
Binary files /dev/null and b/app/assets/images/emoji/minidisc.png differ
diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b661ec1c94a7f080037b8cd1a50c8fa1435d3a5
Binary files /dev/null and b/app/assets/images/emoji/mobile_phone_off.png differ
diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png
new file mode 100644
index 0000000000000000000000000000000000000000..75fd1e90cb0e9bf45e4dd4bc23b19e224df3fa1e
Binary files /dev/null and b/app/assets/images/emoji/money_mouth.png differ
diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png
new file mode 100644
index 0000000000000000000000000000000000000000..f022b04b3c2f0f54902e624d7755f4a29dbeeb70
Binary files /dev/null and b/app/assets/images/emoji/money_with_wings.png differ
diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9296be0902eac2f5344a5d039f34a2d923c81d3
Binary files /dev/null and b/app/assets/images/emoji/moneybag.png differ
diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fae29448e38752d7dd342791ba6d6b0e6f602b7
Binary files /dev/null and b/app/assets/images/emoji/monkey.png differ
diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cab9b91a82bf5ce71c659efe9a5c333c826a460
Binary files /dev/null and b/app/assets/images/emoji/monkey_face.png differ
diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png
new file mode 100644
index 0000000000000000000000000000000000000000..11eb1f574bf7a1eb5907ef8dbc1ddfd9bf3c5599
Binary files /dev/null and b/app/assets/images/emoji/monorail.png differ
diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b17ddd9d002da81c616cbd394b9fac3d6e872c3
Binary files /dev/null and b/app/assets/images/emoji/mortar_board.png differ
diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef770b26d96e8661aa1f417255c7d0628ee3e791
Binary files /dev/null and b/app/assets/images/emoji/mosque.png differ
diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5afa72d8073c67216a13e81c8a9f4411db71976
Binary files /dev/null and b/app/assets/images/emoji/motor_scooter.png differ
diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png
new file mode 100644
index 0000000000000000000000000000000000000000..0506db1a40f6abd89cd83016afede38c2f81f1d2
Binary files /dev/null and b/app/assets/images/emoji/motorboat.png differ
diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d1d567e8ec6ec530001219b1197623c7068b1b8
Binary files /dev/null and b/app/assets/images/emoji/motorcycle.png differ
diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c3d3d03e3f2ef25bbc34046f4b1997b0a4d266e
Binary files /dev/null and b/app/assets/images/emoji/motorway.png differ
diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png
new file mode 100644
index 0000000000000000000000000000000000000000..88a547524581de4fcd8be5db8fcb59d2cda4d583
Binary files /dev/null and b/app/assets/images/emoji/mount_fuji.png differ
diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png
new file mode 100644
index 0000000000000000000000000000000000000000..6722ebdd29463015d6588a8953c11a64bdfddde1
Binary files /dev/null and b/app/assets/images/emoji/mountain.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png
new file mode 100644
index 0000000000000000000000000000000000000000..41d3dc3ac6f80260efa2a31fb927fc5e71c572e0
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9f1daf5e406c5461816c83703562a3ec3eebc73
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist_tone1.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..555b9e29d4dfefc80851ebbd1310c72678daed6b
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist_tone2.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..7df5508ec8c276f9225869a5ff738b3845b0503c
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist_tone3.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..f94b34506970aabf39a7447834bb165137cc498e
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist_tone4.png differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..16a45861e1faf4c3d6f7845fff17cdaa5035f230
Binary files /dev/null and b/app/assets/images/emoji/mountain_bicyclist_tone5.png differ
diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png
new file mode 100644
index 0000000000000000000000000000000000000000..1dea73ca53b517e4e2678f195556bd63230c4643
Binary files /dev/null and b/app/assets/images/emoji/mountain_cableway.png differ
diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png
new file mode 100644
index 0000000000000000000000000000000000000000..ade2218e469707a35ad3c71223fd38eb81c58da3
Binary files /dev/null and b/app/assets/images/emoji/mountain_railway.png differ
diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png
new file mode 100644
index 0000000000000000000000000000000000000000..76e1cfd831378b460c969510a59bce39161e6dde
Binary files /dev/null and b/app/assets/images/emoji/mountain_snow.png differ
diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png
new file mode 100644
index 0000000000000000000000000000000000000000..50afcd3262e3c77eaa5dacf602ade7073a408af5
Binary files /dev/null and b/app/assets/images/emoji/mouse.png differ
diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png
new file mode 100644
index 0000000000000000000000000000000000000000..20fb041f09f5433f8fa26230d32060543701ba9d
Binary files /dev/null and b/app/assets/images/emoji/mouse2.png differ
diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..e84e96ff6e8569ae63f3571658ce159caee52fab
Binary files /dev/null and b/app/assets/images/emoji/mouse_three_button.png differ
diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e73b130155d839e88446f0057e732f0ce090456
Binary files /dev/null and b/app/assets/images/emoji/movie_camera.png differ
diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png
new file mode 100644
index 0000000000000000000000000000000000000000..e6a7779c45bab950cb1656381889e3dec367231d
Binary files /dev/null and b/app/assets/images/emoji/moyai.png differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
new file mode 100644
index 0000000000000000000000000000000000000000..078f0657f9584a5d587c372002d03ce3911c146c
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus.png differ
diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8a695d7035278b2811306cc01b5561c66192306
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus_tone1.png differ
diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e17e8c51f327288ed10b99154c970b22d01bb2b
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus_tone2.png differ
diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3ee4d1dfaeb7c934394a74983e5e05d7b4f476e
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus_tone3.png differ
diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..68a556da2fee2e7c925272ed32e7f43733f8263d
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus_tone4.png differ
diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..ccab3c40ff21ed5b929393e2e9f63bd93ef264f8
Binary files /dev/null and b/app/assets/images/emoji/mrs_claus_tone5.png differ
diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e67c1880f790f32334dd1485e7b2f1fea983d83
Binary files /dev/null and b/app/assets/images/emoji/muscle.png differ
diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1522942ce517bd9979fd23f2309dc25a0aa717e9
Binary files /dev/null and b/app/assets/images/emoji/muscle_tone1.png differ
diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..569c6e832caa1f80a8d94aeb0bf7d7183b08f3d0
Binary files /dev/null and b/app/assets/images/emoji/muscle_tone2.png differ
diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a76b00fa8914bb70d0aab0d0287080781e7b7b7
Binary files /dev/null and b/app/assets/images/emoji/muscle_tone3.png differ
diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..f0cf31328e025610a3bb4f788a2d2928a4dce9b8
Binary files /dev/null and b/app/assets/images/emoji/muscle_tone4.png differ
diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fda92460e836e9c14526665c906179e0dbc801d
Binary files /dev/null and b/app/assets/images/emoji/muscle_tone5.png differ
diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd85742ba2c8e43b48c25fde7ad50865d5992945
Binary files /dev/null and b/app/assets/images/emoji/mushroom.png differ
diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..442b745684257b1b725d0ffc26d86e2b0b1fe389
Binary files /dev/null and b/app/assets/images/emoji/musical_keyboard.png differ
diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png
new file mode 100644
index 0000000000000000000000000000000000000000..06691ef61bbfce4b73a18dedf215b58cd795895e
Binary files /dev/null and b/app/assets/images/emoji/musical_note.png differ
diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png
new file mode 100644
index 0000000000000000000000000000000000000000..47dc05a8ef5184cbc768897c2a224743393e2e29
Binary files /dev/null and b/app/assets/images/emoji/musical_score.png differ
diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c1788e5075a249eb38526c817741cc31bd862e7
Binary files /dev/null and b/app/assets/images/emoji/mute.png differ
diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa52af7050d664481d3c67aa0e9a6f52567d9eae
Binary files /dev/null and b/app/assets/images/emoji/nail_care.png differ
diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..26e883dd244bd9c8fed01f3b16b6056c83d2af4a
Binary files /dev/null and b/app/assets/images/emoji/nail_care_tone1.png differ
diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..61257b47ea323768f391a0bdcbf176a1402820d9
Binary files /dev/null and b/app/assets/images/emoji/nail_care_tone2.png differ
diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..29871b05f625b424b602838273f47dd5be2b53c3
Binary files /dev/null and b/app/assets/images/emoji/nail_care_tone3.png differ
diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2881de0b17d673ba11105ee1ba72a0057c182865
Binary files /dev/null and b/app/assets/images/emoji/nail_care_tone4.png differ
diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0b7c0a45a6dac1caf78500a399d412b838a990e
Binary files /dev/null and b/app/assets/images/emoji/nail_care_tone5.png differ
diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec5ee213e20f18076a3185a010bb75a328391e74
Binary files /dev/null and b/app/assets/images/emoji/name_badge.png differ
diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..a566c109c2818114918551309e51c005dec93bf0
Binary files /dev/null and b/app/assets/images/emoji/nauseated_face.png differ
diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png
new file mode 100644
index 0000000000000000000000000000000000000000..1804e7f3ff33418b26733fed77c0bdc0394926ee
Binary files /dev/null and b/app/assets/images/emoji/necktie.png differ
diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..dae487f1f98dcaa464782781715bf112e130b72e
Binary files /dev/null and b/app/assets/images/emoji/negative_squared_cross_mark.png differ
diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png
new file mode 100644
index 0000000000000000000000000000000000000000..7820bd581dcaf79d4cae0ec1cf794d5ee5136026
Binary files /dev/null and b/app/assets/images/emoji/nerd.png differ
diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..065d193afe495b633267494d4338a0e28b4569f8
Binary files /dev/null and b/app/assets/images/emoji/neutral_face.png differ
diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4f85488d1a87f30b266b3e54b73e9f76b4602c6
Binary files /dev/null and b/app/assets/images/emoji/new.png differ
diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..ecff72caa42b29b364990fe1cb6c062abf84f124
Binary files /dev/null and b/app/assets/images/emoji/new_moon.png differ
diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..150dd12400c05d2509ed2155e23f685b69be81f7
Binary files /dev/null and b/app/assets/images/emoji/new_moon_with_face.png differ
diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png
new file mode 100644
index 0000000000000000000000000000000000000000..2aa8f060bdef5d5f5e94323c1eb93323b34d08ae
Binary files /dev/null and b/app/assets/images/emoji/newspaper.png differ
diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f64748df2b28b1eb093a9d19de1dead3c975fdf6
Binary files /dev/null and b/app/assets/images/emoji/newspaper2.png differ
diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee8d20f5ebc902fbfeee9ffacae68f4ea13a3afe
Binary files /dev/null and b/app/assets/images/emoji/ng.png differ
diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca2018f456d6fbe2bd6b0eeabdbc0bc4e7d792b2
Binary files /dev/null and b/app/assets/images/emoji/night_with_stars.png differ
diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fce3d1eca97bcdcd91b9184dd7d409a9d83c026
Binary files /dev/null and b/app/assets/images/emoji/nine.png differ
diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png
new file mode 100644
index 0000000000000000000000000000000000000000..15cb38dd1e7b0dc3199723cb681eacb6c843ef61
Binary files /dev/null and b/app/assets/images/emoji/no_bell.png differ
diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png
new file mode 100644
index 0000000000000000000000000000000000000000..19c85421ce9d335127647459fc3b412d3196e06e
Binary files /dev/null and b/app/assets/images/emoji/no_bicycles.png differ
diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png
new file mode 100644
index 0000000000000000000000000000000000000000..476800fc5c692b5d47d809f9b49883f2a86e5485
Binary files /dev/null and b/app/assets/images/emoji/no_entry.png differ
diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2efd65e74b977a36db1bfe6792f57de55af5a78
Binary files /dev/null and b/app/assets/images/emoji/no_entry_sign.png differ
diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed5771003228eecc6d2ca0d449eab5d4ac75c901
Binary files /dev/null and b/app/assets/images/emoji/no_good.png differ
diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c1a3cbb8840cea1fa0dfab15e237ab4848cc6a0
Binary files /dev/null and b/app/assets/images/emoji/no_good_tone1.png differ
diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..80d8021f8fe6f2dad8989e433b7ec62b8cb570af
Binary files /dev/null and b/app/assets/images/emoji/no_good_tone2.png differ
diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..635e6a00815012938271389bca3c9e804d9c28f0
Binary files /dev/null and b/app/assets/images/emoji/no_good_tone3.png differ
diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b96e412a3746e21d008b42b699c47776a7b59a00
Binary files /dev/null and b/app/assets/images/emoji/no_good_tone4.png differ
diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a7084afa0a7e779139f083b4c5f66f7669549f7
Binary files /dev/null and b/app/assets/images/emoji/no_good_tone5.png differ
diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b1ae6ea579cc3fe144f5b208cba0c1f6bf75e8e
Binary files /dev/null and b/app/assets/images/emoji/no_mobile_phones.png differ
diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png
new file mode 100644
index 0000000000000000000000000000000000000000..b642f6c117265afa02dc95ed481a9e567b8f82d9
Binary files /dev/null and b/app/assets/images/emoji/no_mouth.png differ
diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png
new file mode 100644
index 0000000000000000000000000000000000000000..286aa577a23b153cc19a73740c41328aae56a000
Binary files /dev/null and b/app/assets/images/emoji/no_pedestrians.png differ
diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png
new file mode 100644
index 0000000000000000000000000000000000000000..586b8d29d05b42799d1e4fbc1da5c57b16753ae5
Binary files /dev/null and b/app/assets/images/emoji/no_smoking.png differ
diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png
new file mode 100644
index 0000000000000000000000000000000000000000..827d4193f4ea480e7e98570add4aad4bf68c1ea0
Binary files /dev/null and b/app/assets/images/emoji/non-potable_water.png differ
diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f04ac5f98fdddbba8b500633861cb43a11bb512
Binary files /dev/null and b/app/assets/images/emoji/nose.png differ
diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8008d17506e3f8dc9fe3f2b01acd3656b8941c68
Binary files /dev/null and b/app/assets/images/emoji/nose_tone1.png differ
diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac17f26e8277dadae5c37e1ba3b32170dc974327
Binary files /dev/null and b/app/assets/images/emoji/nose_tone2.png differ
diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8b6cbe0f8ea3190b7b542deb0bbbc6bd8c913e7
Binary files /dev/null and b/app/assets/images/emoji/nose_tone3.png differ
diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..004b2631e2efc4a72712580ab0062287d2023f2a
Binary files /dev/null and b/app/assets/images/emoji/nose_tone4.png differ
diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b33821f6c9bc88e363a42c5dda6b1f2272f05e8
Binary files /dev/null and b/app/assets/images/emoji/nose_tone5.png differ
diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6c28b4915d5f3402c9470fa7149c0049f00c4a4
Binary files /dev/null and b/app/assets/images/emoji/notebook.png differ
diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png
new file mode 100644
index 0000000000000000000000000000000000000000..03f566b6d2c43c626eea68ae3b9860f600f9c039
Binary files /dev/null and b/app/assets/images/emoji/notebook_with_decorative_cover.png differ
diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png
new file mode 100644
index 0000000000000000000000000000000000000000..85faa10d8eaa9db3a1f268ff7ec083e220f3bad0
Binary files /dev/null and b/app/assets/images/emoji/notepad_spiral.png differ
diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png
new file mode 100644
index 0000000000000000000000000000000000000000..57d499aa181a22204a6e575efd825796cdb53c97
Binary files /dev/null and b/app/assets/images/emoji/notes.png differ
diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b9ae1553191f4c38d137372bc22ea80d09a940b
Binary files /dev/null and b/app/assets/images/emoji/nut_and_bolt.png differ
diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png
new file mode 100644
index 0000000000000000000000000000000000000000..3fe75ce4675c12d9a53a181d5e67f8bfcd3e00e9
Binary files /dev/null and b/app/assets/images/emoji/o.png differ
diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png
new file mode 100644
index 0000000000000000000000000000000000000000..73278ba194a6510be1d4947002f733fca39d5d59
Binary files /dev/null and b/app/assets/images/emoji/o2.png differ
diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png
new file mode 100644
index 0000000000000000000000000000000000000000..45ff1e8770303c5f41370ffcfa41462f2c1ab643
Binary files /dev/null and b/app/assets/images/emoji/ocean.png differ
diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ed6100404587e59eddbb19b7ab26b6249094ecb
Binary files /dev/null and b/app/assets/images/emoji/octagonal_sign.png differ
diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png
new file mode 100644
index 0000000000000000000000000000000000000000..72c84074aac779d05b7423f966937bc7ddde5e55
Binary files /dev/null and b/app/assets/images/emoji/octopus.png differ
diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png
new file mode 100644
index 0000000000000000000000000000000000000000..d38a849fece3b0c6ca7c9d7025115e076fbde42d
Binary files /dev/null and b/app/assets/images/emoji/oden.png differ
diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png
new file mode 100644
index 0000000000000000000000000000000000000000..7eee927d1b085e1700ce9ff0ebf6a481099ab204
Binary files /dev/null and b/app/assets/images/emoji/office.png differ
diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4c4d42da8b8bf02b2390bd5cdc85c3400e6da0e
Binary files /dev/null and b/app/assets/images/emoji/oil.png differ
diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0d775532ff9b55f830e1b309a0be383fb1288a5
Binary files /dev/null and b/app/assets/images/emoji/ok.png differ
diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png
new file mode 100644
index 0000000000000000000000000000000000000000..028d69b0de312aa29b1975ff07cfc6236a688ca9
Binary files /dev/null and b/app/assets/images/emoji/ok_hand.png differ
diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..cecf7b2ab5aa617c9ea08730f6449d9741cfbb9e
Binary files /dev/null and b/app/assets/images/emoji/ok_hand_tone1.png differ
diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c19239bcd3d96aca86eded11edaa6be76254c30d
Binary files /dev/null and b/app/assets/images/emoji/ok_hand_tone2.png differ
diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..94b65b03ecd1ced03615da9fc1a892f1299cbd8f
Binary files /dev/null and b/app/assets/images/emoji/ok_hand_tone3.png differ
diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..03d26f08e6a859ba7c8bfe11e448503c61bd9511
Binary files /dev/null and b/app/assets/images/emoji/ok_hand_tone4.png differ
diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4b24086364c87094bb2a6851210860038b9bf60
Binary files /dev/null and b/app/assets/images/emoji/ok_hand_tone5.png differ
diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png
new file mode 100644
index 0000000000000000000000000000000000000000..90a2c7469c4bdde28a997545287a5225392dcd89
Binary files /dev/null and b/app/assets/images/emoji/ok_woman.png differ
diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c99543e785bd967ff5849c19a5b3d1fa9c6bc090
Binary files /dev/null and b/app/assets/images/emoji/ok_woman_tone1.png differ
diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad5fae813dbc8ebd7bea878b41dfa895fd485d50
Binary files /dev/null and b/app/assets/images/emoji/ok_woman_tone2.png differ
diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..51bf4fab4062c57bfb8ecab19939c0b7fbf25bc3
Binary files /dev/null and b/app/assets/images/emoji/ok_woman_tone3.png differ
diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee3f9dc640ab02e90fce937773723dea1bcd85a2
Binary files /dev/null and b/app/assets/images/emoji/ok_woman_tone4.png differ
diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..62a9d9237f7c56b14a493d4106f1b0b4bd8bb21a
Binary files /dev/null and b/app/assets/images/emoji/ok_woman_tone5.png differ
diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ace4e6f308f97fc578b16e28194d804116a9fea
Binary files /dev/null and b/app/assets/images/emoji/older_man.png differ
diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab459baace85a80857438a047c7f3a8283cc2724
Binary files /dev/null and b/app/assets/images/emoji/older_man_tone1.png differ
diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4dfc7694eac7cf44af625b5ce00adef87dd562b
Binary files /dev/null and b/app/assets/images/emoji/older_man_tone2.png differ
diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ffd11792f4717a9b287a1777acc0e1303a6ad7f
Binary files /dev/null and b/app/assets/images/emoji/older_man_tone3.png differ
diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b350a764bfdc8909c8b5b5ce75a9cd132d16259b
Binary files /dev/null and b/app/assets/images/emoji/older_man_tone4.png differ
diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..05fe24a17083c7db19076a34ba98c0fc222158e9
Binary files /dev/null and b/app/assets/images/emoji/older_man_tone5.png differ
diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png
new file mode 100644
index 0000000000000000000000000000000000000000..52dc4987143a75762c00d6260df08879422b11ee
Binary files /dev/null and b/app/assets/images/emoji/older_woman.png differ
diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..b49e821402cfa1409468ecdb90f91daa2893dd75
Binary files /dev/null and b/app/assets/images/emoji/older_woman_tone1.png differ
diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..e86bf5ab3b786f0b6f605a5d34595380141beca4
Binary files /dev/null and b/app/assets/images/emoji/older_woman_tone2.png differ
diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..83fc14b08749a017ef98134423c23a4e3bac3698
Binary files /dev/null and b/app/assets/images/emoji/older_woman_tone3.png differ
diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4aa8a424d45abdc84b7f4de35689ec0ee7e7e94
Binary files /dev/null and b/app/assets/images/emoji/older_woman_tone4.png differ
diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4009012bb0ad7bdef808aa11c19ca6cb329fbd9a
Binary files /dev/null and b/app/assets/images/emoji/older_woman_tone5.png differ
diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png
new file mode 100644
index 0000000000000000000000000000000000000000..a35c63c459cb251d046e972e2a2f4549695d2169
Binary files /dev/null and b/app/assets/images/emoji/om_symbol.png differ
diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0c371ae21e1eb96680415885f4fe860b1d857f1
Binary files /dev/null and b/app/assets/images/emoji/on.png differ
diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c7e1d52e63d7a03815c3e49719cd40c1f2c768b
Binary files /dev/null and b/app/assets/images/emoji/oncoming_automobile.png differ
diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad91e256c7f8ea15617fabbf8c2e6aad323fea70
Binary files /dev/null and b/app/assets/images/emoji/oncoming_bus.png differ
diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9109c85b5d9f3ae7957ac584c3083d4070b70b5
Binary files /dev/null and b/app/assets/images/emoji/oncoming_police_car.png differ
diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png
new file mode 100644
index 0000000000000000000000000000000000000000..fea14e45846422aab5340729e5a417611acd6f44
Binary files /dev/null and b/app/assets/images/emoji/oncoming_taxi.png differ
diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png
new file mode 100644
index 0000000000000000000000000000000000000000..e6d84b80128c49372e0623cdb7c136c580925361
Binary files /dev/null and b/app/assets/images/emoji/one.png differ
diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png
new file mode 100644
index 0000000000000000000000000000000000000000..3993b09222f3d29401b690ac8ad2606d3ac5ac51
Binary files /dev/null and b/app/assets/images/emoji/open_file_folder.png differ
diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png
new file mode 100644
index 0000000000000000000000000000000000000000..1cf75c9101ed902772f48b8e4bf0ca2715683da8
Binary files /dev/null and b/app/assets/images/emoji/open_hands.png differ
diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..352d2614f11b5ed64750835ae81e8ffb7d119019
Binary files /dev/null and b/app/assets/images/emoji/open_hands_tone1.png differ
diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..70824a50c73da9d145fb2a7898a80c8a8fe3d6d7
Binary files /dev/null and b/app/assets/images/emoji/open_hands_tone2.png differ
diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7d136bd3db157b4117bf7f8698feaf407e958fb
Binary files /dev/null and b/app/assets/images/emoji/open_hands_tone3.png differ
diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..df4eaa711e7003cfb50f93a4e054623d086937a6
Binary files /dev/null and b/app/assets/images/emoji/open_hands_tone4.png differ
diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..7dc04eaebd83d1af61e73c01e1ff5af7b68cd99f
Binary files /dev/null and b/app/assets/images/emoji/open_hands_tone5.png differ
diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png
new file mode 100644
index 0000000000000000000000000000000000000000..a62cd27e148591226185d5794848fed00642f428
Binary files /dev/null and b/app/assets/images/emoji/open_mouth.png differ
diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a780a700daaa32127062dd1b460609173ff3234
Binary files /dev/null and b/app/assets/images/emoji/ophiuchus.png differ
diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab40e6ae6a246a09ab5758fe085e6ed289152521
Binary files /dev/null and b/app/assets/images/emoji/orange_book.png differ
diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png
new file mode 100644
index 0000000000000000000000000000000000000000..0530e33a4d42b2c5cf66adc5171acb42626b0a6a
Binary files /dev/null and b/app/assets/images/emoji/orthodox_cross.png differ
diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png
new file mode 100644
index 0000000000000000000000000000000000000000..46493ed5b2ce8751325338cb85b0dea99f5942a3
Binary files /dev/null and b/app/assets/images/emoji/outbox_tray.png differ
diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa6815480c34518bd52f8e31ad7de19659dbcd45
Binary files /dev/null and b/app/assets/images/emoji/owl.png differ
diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png
new file mode 100644
index 0000000000000000000000000000000000000000..badf5708f2fdb0be65f4a444f21431a52b3fcd56
Binary files /dev/null and b/app/assets/images/emoji/ox.png differ
diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png
new file mode 100644
index 0000000000000000000000000000000000000000..85431756ad8c548ed7a0f095515caf58b4b11f21
Binary files /dev/null and b/app/assets/images/emoji/package.png differ
diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba4ed757e01f3b8d119d938e0750efaba0033b7b
Binary files /dev/null and b/app/assets/images/emoji/page_facing_up.png differ
diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png
new file mode 100644
index 0000000000000000000000000000000000000000..06355319c7432a4137bcce2d8dcd09cbbbcf22f2
Binary files /dev/null and b/app/assets/images/emoji/page_with_curl.png differ
diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png
new file mode 100644
index 0000000000000000000000000000000000000000..b24b99306a243de292e6de493790eab996a64741
Binary files /dev/null and b/app/assets/images/emoji/pager.png differ
diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png
new file mode 100644
index 0000000000000000000000000000000000000000..28bffbaa3c92f0210c599baa95f353b50de9bb1f
Binary files /dev/null and b/app/assets/images/emoji/paintbrush.png differ
diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bbb10f4f19dd17454d85cfeaf44d18140837a41
Binary files /dev/null and b/app/assets/images/emoji/palm_tree.png differ
diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png
new file mode 100644
index 0000000000000000000000000000000000000000..6223d1a28e99c8fd4d6d6c3fbc65127353173143
Binary files /dev/null and b/app/assets/images/emoji/pancakes.png differ
diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..978382775ceca17bcf605e1604b16a22e59a006a
Binary files /dev/null and b/app/assets/images/emoji/panda_face.png differ
diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png
new file mode 100644
index 0000000000000000000000000000000000000000..8cd8d4f87506f259461970674f0338ac74deec73
Binary files /dev/null and b/app/assets/images/emoji/paperclip.png differ
diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png
new file mode 100644
index 0000000000000000000000000000000000000000..76021e8c70599b74a5805696917d723df97ed904
Binary files /dev/null and b/app/assets/images/emoji/paperclips.png differ
diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png
new file mode 100644
index 0000000000000000000000000000000000000000..63ec7016301a449fd6eb0fbfb30400880a25f003
Binary files /dev/null and b/app/assets/images/emoji/park.png differ
diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png
new file mode 100644
index 0000000000000000000000000000000000000000..7be7dac27e89e78da8bc4ae3ebebd0ae85dbd268
Binary files /dev/null and b/app/assets/images/emoji/parking.png differ
diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..70453d41528dfcab0837d0b2bc33e01217286800
Binary files /dev/null and b/app/assets/images/emoji/part_alternation_mark.png differ
diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png
new file mode 100644
index 0000000000000000000000000000000000000000..a55e59c344cf9de7fa113da9fc17bd459eb3daf5
Binary files /dev/null and b/app/assets/images/emoji/partly_sunny.png differ
diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png
new file mode 100644
index 0000000000000000000000000000000000000000..079e34ee4d41eb872db81a81ff2311ac2b2322f6
Binary files /dev/null and b/app/assets/images/emoji/passport_control.png differ
diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f07e7ebfd7b3a04aa4004b27414fad8b2af9ef1
Binary files /dev/null and b/app/assets/images/emoji/pause_button.png differ
diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png
new file mode 100644
index 0000000000000000000000000000000000000000..86033faf477fd294e08a95a08b55c1cbe263f5f6
Binary files /dev/null and b/app/assets/images/emoji/peace.png differ
diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ab57cbb75813895b596185f3d412f532cae2152
Binary files /dev/null and b/app/assets/images/emoji/peach.png differ
diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png
new file mode 100644
index 0000000000000000000000000000000000000000..b64fadad01090daf70703d0dfd94e48a91a2421b
Binary files /dev/null and b/app/assets/images/emoji/peanuts.png differ
diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png
new file mode 100644
index 0000000000000000000000000000000000000000..3869f718bcf472ad3b2e2fd42e5f9bb33d48b5aa
Binary files /dev/null and b/app/assets/images/emoji/pear.png differ
diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ef7a3424338ee29eeb2f0b8ff1b330da5a1ff1c
Binary files /dev/null and b/app/assets/images/emoji/pen_ballpoint.png differ
diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ca4bd2c231a5f5757a640d331bf24e62b85a66b
Binary files /dev/null and b/app/assets/images/emoji/pen_fountain.png differ
diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png
new file mode 100644
index 0000000000000000000000000000000000000000..edc6155e168b8a011b6c2da1d0d2e45e2e2e0c6f
Binary files /dev/null and b/app/assets/images/emoji/pencil.png differ
diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png
new file mode 100644
index 0000000000000000000000000000000000000000..3833d590fa2ce48cd2409b77041ff0d15cbeec3c
Binary files /dev/null and b/app/assets/images/emoji/pencil2.png differ
diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0064fb9734bf3779294e1727817fcd16fed9f95
Binary files /dev/null and b/app/assets/images/emoji/penguin.png differ
diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png
new file mode 100644
index 0000000000000000000000000000000000000000..490fb5669549cd1172f86a45d8e9966b5ea20846
Binary files /dev/null and b/app/assets/images/emoji/pensive.png differ
diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png
new file mode 100644
index 0000000000000000000000000000000000000000..685441fdaa11808fd199ac3577d0075cde5ab0c5
Binary files /dev/null and b/app/assets/images/emoji/performing_arts.png differ
diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png
new file mode 100644
index 0000000000000000000000000000000000000000..646a05fe908a3bd2a098f53da9cf8fbc0e35e9b5
Binary files /dev/null and b/app/assets/images/emoji/persevere.png differ
diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png
new file mode 100644
index 0000000000000000000000000000000000000000..579324959a18a8aeae31a67b5dc11e07ce25107b
Binary files /dev/null and b/app/assets/images/emoji/person_frowning.png differ
diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..21d3bb4392340d7cb8e4cccca755c714c033595d
Binary files /dev/null and b/app/assets/images/emoji/person_frowning_tone1.png differ
diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..973f5fc8382cc604dbeec1de287480f0ad3c8400
Binary files /dev/null and b/app/assets/images/emoji/person_frowning_tone2.png differ
diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..41fbcc78816c0623476798a865100883fa36c366
Binary files /dev/null and b/app/assets/images/emoji/person_frowning_tone3.png differ
diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a37c741030ee351836a0f8a4118ed045247fe13
Binary files /dev/null and b/app/assets/images/emoji/person_frowning_tone4.png differ
diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..e08141f3efec72c4fcfc0bdd3b422000055a4174
Binary files /dev/null and b/app/assets/images/emoji/person_frowning_tone5.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad6f01a7ddae22e00bdc5a0915333aa10c867b25
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d18ef24445e417900c386f904b43622bf6d76a7
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair_tone1.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..dae1307315ccb976205c81d855c0c40a41547678
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair_tone2.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..684677e8e5a861a966eb2655756d2d089c0d64e2
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair_tone3.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..012be0b51f8164458a454d6e71b23fcf82edb8a9
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair_tone4.png differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4ecc4cf44ba0b4a02e16e8f934ce98ea6ef604d
Binary files /dev/null and b/app/assets/images/emoji/person_with_blond_hair_tone5.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..10eb057107814f5b12f8d45907cc472e386f57eb
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..57e826b75a4e672911c6a1d3e1775a904762c66d
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face_tone1.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..3f317c0c25fd2f0122aaba9568ad3ea7c7f69f00
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face_tone2.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2fbb6c20bfae5d39d1f0ba70a6dc94d5ca320ba
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face_tone3.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..643ceb4a5c5f4c7a1cb0572c43e4f05309349f13
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face_tone4.png differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2eb6859c32c4887275683c9dc5affa269b1987d
Binary files /dev/null and b/app/assets/images/emoji/person_with_pouting_face_tone5.png differ
diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png
new file mode 100644
index 0000000000000000000000000000000000000000..6370fe6d79175acb276514a3591c3bd3fc38e796
Binary files /dev/null and b/app/assets/images/emoji/pick.png differ
diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png
new file mode 100644
index 0000000000000000000000000000000000000000..afe05ca167662d701e8fcf2c5cf080c5bce9df5f
Binary files /dev/null and b/app/assets/images/emoji/pig.png differ
diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f31c1a2d757b8f8aa471a5bbec50d05713ab814
Binary files /dev/null and b/app/assets/images/emoji/pig2.png differ
diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png
new file mode 100644
index 0000000000000000000000000000000000000000..3610ae4a9103334e3c88cc989b06eb3d51ec6e21
Binary files /dev/null and b/app/assets/images/emoji/pig_nose.png differ
diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png
new file mode 100644
index 0000000000000000000000000000000000000000..1d4530e77a32732ef25e746563a0b55929929108
Binary files /dev/null and b/app/assets/images/emoji/pill.png differ
diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png
new file mode 100644
index 0000000000000000000000000000000000000000..c89a16064626aa94fbf643409a9bdcdaf4efa99f
Binary files /dev/null and b/app/assets/images/emoji/pineapple.png differ
diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff3c51727d18679a4c9f6a9e5f64d1c98e41aa2f
Binary files /dev/null and b/app/assets/images/emoji/ping_pong.png differ
diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f6f646a95c5f6751e95127fe8e07b98b89c2384
Binary files /dev/null and b/app/assets/images/emoji/pisces.png differ
diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png
new file mode 100644
index 0000000000000000000000000000000000000000..e07365cb398c0c0f494e9cfee705ceb91e4a7ee6
Binary files /dev/null and b/app/assets/images/emoji/pizza.png differ
diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png
new file mode 100644
index 0000000000000000000000000000000000000000..207d59cce851eed61782c0845e5404db471e2042
Binary files /dev/null and b/app/assets/images/emoji/place_of_worship.png differ
diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9f857139ac0d975fafb8e4b542dcdc310baf8d9
Binary files /dev/null and b/app/assets/images/emoji/play_pause.png differ
diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..00d3d13ab5c09eca54b584f5a6c456bb6cf0a30b
Binary files /dev/null and b/app/assets/images/emoji/point_down.png differ
diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..140f157d8c7baea9016203cb24111078828574c7
Binary files /dev/null and b/app/assets/images/emoji/point_down_tone1.png differ
diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..d518544f7fafad6192852f9edd46dde9deab14bd
Binary files /dev/null and b/app/assets/images/emoji/point_down_tone2.png differ
diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..018b688b8b7d6ea88cea7db918ed5370221e17d7
Binary files /dev/null and b/app/assets/images/emoji/point_down_tone3.png differ
diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..98845bf6f728a09af42a1057e7423e8b7bc32c87
Binary files /dev/null and b/app/assets/images/emoji/point_down_tone4.png differ
diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a9b039a9fc1cc3d2899047207234fee38774590
Binary files /dev/null and b/app/assets/images/emoji/point_down_tone5.png differ
diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png
new file mode 100644
index 0000000000000000000000000000000000000000..599fa2e3cf12fc090d1d0c23da5af857878a3818
Binary files /dev/null and b/app/assets/images/emoji/point_left.png differ
diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..88e2c3060765d8c96a75e43f700b0695c7886cdc
Binary files /dev/null and b/app/assets/images/emoji/point_left_tone1.png differ
diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3c89d87c5f9c035bf22442e3b0e831d5afa93b5
Binary files /dev/null and b/app/assets/images/emoji/point_left_tone2.png differ
diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b23b9167358d8a8861becf4dd36f3f7a3136a676
Binary files /dev/null and b/app/assets/images/emoji/point_left_tone3.png differ
diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..3093f325c272f8b0409863e743532fa734483c06
Binary files /dev/null and b/app/assets/images/emoji/point_left_tone4.png differ
diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b4cbfa120c818f3e68589cc50f5d4f36c8a704f
Binary files /dev/null and b/app/assets/images/emoji/point_left_tone5.png differ
diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..93a3cd34aa5bad031faa771925d0acb097eb1bf9
Binary files /dev/null and b/app/assets/images/emoji/point_right.png differ
diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a28c6bbc8998dc91d6b518d138840cd226f2051
Binary files /dev/null and b/app/assets/images/emoji/point_right_tone1.png differ
diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cb132317333247a2fbc72b5c24bf455b67a0df3
Binary files /dev/null and b/app/assets/images/emoji/point_right_tone2.png differ
diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5514807d71a2ac1e308046d5e339bbf0ba297e0f
Binary files /dev/null and b/app/assets/images/emoji/point_right_tone3.png differ
diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8541d6440daec3a12e9cbea848f55296e999e2d
Binary files /dev/null and b/app/assets/images/emoji/point_right_tone4.png differ
diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b7aab07bb12bf36cfb3e3be2aa1a849cda01a4c
Binary files /dev/null and b/app/assets/images/emoji/point_right_tone5.png differ
diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4978ff0f003e9b99374686562cc593ce07947b8
Binary files /dev/null and b/app/assets/images/emoji/point_up.png differ
diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc496dfeae4e64fc3497a14b6af2da6ee22e98ce
Binary files /dev/null and b/app/assets/images/emoji/point_up_2.png differ
diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..a12a7e784301f5826ca05c4f2b61922906c063ee
Binary files /dev/null and b/app/assets/images/emoji/point_up_2_tone1.png differ
diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdff40ceab03f6ddc501f58ca9f45c99d3be942c
Binary files /dev/null and b/app/assets/images/emoji/point_up_2_tone2.png differ
diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a07ce9e5ae8ce95ae3e30769804df5bf783a35b4
Binary files /dev/null and b/app/assets/images/emoji/point_up_2_tone3.png differ
diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f86c88ba42c53ec512d679580818f68612ffc2d
Binary files /dev/null and b/app/assets/images/emoji/point_up_2_tone4.png differ
diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed1b26c35d3f3efbd3b09729644dc4b60863ca4c
Binary files /dev/null and b/app/assets/images/emoji/point_up_2_tone5.png differ
diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a9db21d64ce04d2c71e6261e264811ad2da77ee
Binary files /dev/null and b/app/assets/images/emoji/point_up_tone1.png differ
diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..15aa9ea0e05fd9b1090f7723db1cde11118f637c
Binary files /dev/null and b/app/assets/images/emoji/point_up_tone2.png differ
diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..652b73a9c5d329af71100b2b0cedd5a75a491562
Binary files /dev/null and b/app/assets/images/emoji/point_up_tone3.png differ
diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..692bad926e9ed6b2d3a8483f8f8db12bfec809d0
Binary files /dev/null and b/app/assets/images/emoji/point_up_tone4.png differ
diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e1b10fb71ccff8652021237d0032a9a18905656
Binary files /dev/null and b/app/assets/images/emoji/point_up_tone5.png differ
diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..3da4253de7e8e33124f9875dada7969ca6fab07b
Binary files /dev/null and b/app/assets/images/emoji/police_car.png differ
diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ec39e396af95a2e5926fc724984ca3d24a68323
Binary files /dev/null and b/app/assets/images/emoji/poodle.png differ
diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png
new file mode 100644
index 0000000000000000000000000000000000000000..10b15e72d56575233274720ce1122da9fb0f1a9d
Binary files /dev/null and b/app/assets/images/emoji/poop.png differ
diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png
new file mode 100644
index 0000000000000000000000000000000000000000..36853e381d4cb1707f7b06aa5b4b6ea7e1873b72
Binary files /dev/null and b/app/assets/images/emoji/popcorn.png differ
diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png
new file mode 100644
index 0000000000000000000000000000000000000000..a23848f9aa0a657b9e7d47f36d2ee720cc2e8ce2
Binary files /dev/null and b/app/assets/images/emoji/post_office.png differ
diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png
new file mode 100644
index 0000000000000000000000000000000000000000..c173b8dbd67d050a33265f79fbe251cbc63dd9f3
Binary files /dev/null and b/app/assets/images/emoji/postal_horn.png differ
diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png
new file mode 100644
index 0000000000000000000000000000000000000000..07c9c4ab3d6e32d338cf8099d649d45c47ac7373
Binary files /dev/null and b/app/assets/images/emoji/postbox.png differ
diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c610049459d85433371a267713849b0f1202849
Binary files /dev/null and b/app/assets/images/emoji/potable_water.png differ
diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png
new file mode 100644
index 0000000000000000000000000000000000000000..70350ca2c0aada043a21968f5dd0a92fdcffa0ff
Binary files /dev/null and b/app/assets/images/emoji/potato.png differ
diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png
new file mode 100644
index 0000000000000000000000000000000000000000..8795c6c66ff3d75f3381b27b28e741edffa28393
Binary files /dev/null and b/app/assets/images/emoji/pouch.png differ
diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png
new file mode 100644
index 0000000000000000000000000000000000000000..eea4a53a2f9908386e479618d6bc18f9cbbdfd00
Binary files /dev/null and b/app/assets/images/emoji/poultry_leg.png differ
diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0d4c4099e9113d484f1721a756246a6d0f8f2ba
Binary files /dev/null and b/app/assets/images/emoji/pound.png differ
diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..41ddfeab42b452efb03fb37236939d0033e0f90c
Binary files /dev/null and b/app/assets/images/emoji/pouting_cat.png differ
diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png
new file mode 100644
index 0000000000000000000000000000000000000000..8347f2435bee8ee528db5e16e09df6c551c96330
Binary files /dev/null and b/app/assets/images/emoji/pray.png differ
diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..060ef257172069b192d23872bcc3740c98532f45
Binary files /dev/null and b/app/assets/images/emoji/pray_tone1.png differ
diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..56dc607c07a70455d3af085691cd5fa6408db878
Binary files /dev/null and b/app/assets/images/emoji/pray_tone2.png differ
diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f33b862008e7adda5afab1604f6ace9f2677f11
Binary files /dev/null and b/app/assets/images/emoji/pray_tone3.png differ
diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ea8dc116572ba7e7e93acec414a565cd497d35f
Binary files /dev/null and b/app/assets/images/emoji/pray_tone4.png differ
diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..2128a6c4703af5209797eb1f36fe0ac1244b0db2
Binary files /dev/null and b/app/assets/images/emoji/pray_tone5.png differ
diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4b6dfcc62ef4df9cc0233b82af23eb48749f309
Binary files /dev/null and b/app/assets/images/emoji/prayer_beads.png differ
diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png
new file mode 100644
index 0000000000000000000000000000000000000000..084e83a414a334224ec0a4395226ec9bf84a0fda
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman.png differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..a78703b33aad0d00759b9ed071ed526c162a643b
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman_tone1.png differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0068c6c4a7727e796836b25d39a3d4755bc18566
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman_tone2.png differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..3206296b684d9753da4e806db4c9874150b10a8f
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman_tone3.png differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..120fda5cd8c16abcb3edd0977b00ebfcc209e9df
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman_tone4.png differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..569bfdf05ce28b5251ea91d66b6a5341dac13091
Binary files /dev/null and b/app/assets/images/emoji/pregnant_woman_tone5.png differ
diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png
new file mode 100644
index 0000000000000000000000000000000000000000..38d69344c84c3f1f4b4b6475e91e57e2e03d0e16
Binary files /dev/null and b/app/assets/images/emoji/prince.png differ
diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..849930c888773828022a7aeb27dab83565f348a8
Binary files /dev/null and b/app/assets/images/emoji/prince_tone1.png differ
diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..23d8b3b12856be9fc0814a107e16b9a4c0623947
Binary files /dev/null and b/app/assets/images/emoji/prince_tone2.png differ
diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..db6dfff0647e5c3adb63a87067c26a105b85571f
Binary files /dev/null and b/app/assets/images/emoji/prince_tone3.png differ
diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e10f8be6a8d06ae5113d4c02cfff857da92da14
Binary files /dev/null and b/app/assets/images/emoji/prince_tone4.png differ
diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..138d4ea7048540b4426eb843eb49a4802673bc56
Binary files /dev/null and b/app/assets/images/emoji/prince_tone5.png differ
diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png
new file mode 100644
index 0000000000000000000000000000000000000000..879e9fa8c5d2918a9e7f7352b4af99b3d14833a3
Binary files /dev/null and b/app/assets/images/emoji/princess.png differ
diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c28078cdc36bd94aff9c97f7f9d165732521ac02
Binary files /dev/null and b/app/assets/images/emoji/princess_tone1.png differ
diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..dcd20e6ecd481bff68a9867e6da759b11a5f2327
Binary files /dev/null and b/app/assets/images/emoji/princess_tone2.png differ
diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..cde6f315c56a5f57c159ca869ac8627316ba1c8e
Binary files /dev/null and b/app/assets/images/emoji/princess_tone3.png differ
diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..c71e69caaef997d2536f1b3fa261fb954dae8f52
Binary files /dev/null and b/app/assets/images/emoji/princess_tone4.png differ
diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..063e264591054f1ce20ae3608ed97b635241d233
Binary files /dev/null and b/app/assets/images/emoji/princess_tone5.png differ
diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png
new file mode 100644
index 0000000000000000000000000000000000000000..027c830f0fe217ebfb5134d4ede480e7048718b0
Binary files /dev/null and b/app/assets/images/emoji/printer.png differ
diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce9ab0daa28eddda7d67c4eef4f39b7f68c82ad2
Binary files /dev/null and b/app/assets/images/emoji/projector.png differ
diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png
new file mode 100644
index 0000000000000000000000000000000000000000..b14ca5f5211a894d2189041e3077a489f75281fe
Binary files /dev/null and b/app/assets/images/emoji/punch.png differ
diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..93c7d17fb472456b594e6dff1ac15c441410d968
Binary files /dev/null and b/app/assets/images/emoji/punch_tone1.png differ
diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0a1af6e10a787901704db36f50a3ff25d7b259d
Binary files /dev/null and b/app/assets/images/emoji/punch_tone2.png differ
diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..1458b0212010f063e44fe9dcf6d4235ec936c7bb
Binary files /dev/null and b/app/assets/images/emoji/punch_tone3.png differ
diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1466bfcdef229401dee81e1ec67c3db2b13cceb
Binary files /dev/null and b/app/assets/images/emoji/punch_tone4.png differ
diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..00b4ddb8953973e152e012ec9945181e08b055c6
Binary files /dev/null and b/app/assets/images/emoji/punch_tone5.png differ
diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..95c53a9ade6db13f2dcfdac83d5ae4dda2f40e88
Binary files /dev/null and b/app/assets/images/emoji/purple_heart.png differ
diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png
new file mode 100644
index 0000000000000000000000000000000000000000..981346193c5e8d038c6aed130d7e790807646284
Binary files /dev/null and b/app/assets/images/emoji/purse.png differ
diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png
new file mode 100644
index 0000000000000000000000000000000000000000..57e07d7f4ccacd1e6c22389f804df7566e9a8318
Binary files /dev/null and b/app/assets/images/emoji/pushpin.png differ
diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png
new file mode 100644
index 0000000000000000000000000000000000000000..82a84f9a37589ec8e52b4eeddb8ad539a19c65ed
Binary files /dev/null and b/app/assets/images/emoji/put_litter_in_its_place.png differ
diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a58f3458aa16a8697cd5bb9ecc4ec5c4517b0f7
Binary files /dev/null and b/app/assets/images/emoji/question.png differ
diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea75ab0426e67fa961cbdf6c6bd5b7f499048adb
Binary files /dev/null and b/app/assets/images/emoji/rabbit.png differ
diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c8a29c642f48d3fea41769f9fb69f37a99891fe
Binary files /dev/null and b/app/assets/images/emoji/rabbit2.png differ
diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe3f045f446625d23bf18dbfd9be83cdbcc36b11
Binary files /dev/null and b/app/assets/images/emoji/race_car.png differ
diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3e73cc8903acecc68786c11e042320643e6cfeb
Binary files /dev/null and b/app/assets/images/emoji/racehorse.png differ
diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png
new file mode 100644
index 0000000000000000000000000000000000000000..dec381fa2427fc700886549377f466e8eabd6e36
Binary files /dev/null and b/app/assets/images/emoji/radio.png differ
diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a23449d917fd726ae371ad8ee7cc8d52e48ca7a
Binary files /dev/null and b/app/assets/images/emoji/radio_button.png differ
diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b46199fe374c1d1f53f96844387f5cfd3df5591
Binary files /dev/null and b/app/assets/images/emoji/radioactive.png differ
diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d739bd40adbba997b35bbdd20b50e85c3c0e3db
Binary files /dev/null and b/app/assets/images/emoji/rage.png differ
diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9acbf1300811c2c67e031fed0dfa56f4e5ff9b4
Binary files /dev/null and b/app/assets/images/emoji/railway_car.png differ
diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1a7a0d14309f1d0ce4a14dc193dcc601b160579
Binary files /dev/null and b/app/assets/images/emoji/railway_track.png differ
diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png
new file mode 100644
index 0000000000000000000000000000000000000000..154735d7147bcd673d2d7dfc81697cb371fe02ba
Binary files /dev/null and b/app/assets/images/emoji/rainbow.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png
new file mode 100644
index 0000000000000000000000000000000000000000..479234294b4833dfb8dc88b496b40187536c84ff
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..813d28499b5217172136f02f4ee9778bff785cb2
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand_tone1.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..192ff795e37c0386ae2bc15fd7cb7ee3b83f8ce3
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand_tone2.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..61a727abe6baf27d86e2ef7c9ab86a1fe52c8dbd
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand_tone3.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e83da511f55e9e3270c0fbb84e851557e88a34a
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand_tone4.png differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7a5b95a02c7ae9f75e2053ecb21e80bab55dd7c
Binary files /dev/null and b/app/assets/images/emoji/raised_back_of_hand_tone5.png differ
diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b2954315d18c545eb74256466020cdd36c10cd7
Binary files /dev/null and b/app/assets/images/emoji/raised_hand.png differ
diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b752902c07c64e32ecf91a04580250f2c9f3836
Binary files /dev/null and b/app/assets/images/emoji/raised_hand_tone1.png differ
diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..44e2a514c600c9a5208fc829fe8091537a112c9a
Binary files /dev/null and b/app/assets/images/emoji/raised_hand_tone2.png differ
diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bb62a7528ac99218b4f877b2578a84ac1535bb1
Binary files /dev/null and b/app/assets/images/emoji/raised_hand_tone3.png differ
diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7f8c9ec2701f1d87e89b78b27d8d6a548a8c1b3
Binary files /dev/null and b/app/assets/images/emoji/raised_hand_tone4.png differ
diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c601b58a73e93565e959a9d92b912c9a018733ea
Binary files /dev/null and b/app/assets/images/emoji/raised_hand_tone5.png differ
diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0155f728e7854adac1b257ff39c8be45f491aab
Binary files /dev/null and b/app/assets/images/emoji/raised_hands.png differ
diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1168b8236b646648ce41ca9609f237d41324f4ef
Binary files /dev/null and b/app/assets/images/emoji/raised_hands_tone1.png differ
diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..322de622903bea9e10e7c04860337e7342ab3005
Binary files /dev/null and b/app/assets/images/emoji/raised_hands_tone2.png differ
diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..2aa24e05ae18a6456ccaf68e722c202eb4c5e6e4
Binary files /dev/null and b/app/assets/images/emoji/raised_hands_tone3.png differ
diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..f31bf0db992fd73be1aac0c6d0358e991b9ab97d
Binary files /dev/null and b/app/assets/images/emoji/raised_hands_tone4.png differ
diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e95067f98b13aed818eff07d45320e113a05026
Binary files /dev/null and b/app/assets/images/emoji/raised_hands_tone5.png differ
diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png
new file mode 100644
index 0000000000000000000000000000000000000000..2880708c0cc73561ceadd8d209e7fbe9d5960126
Binary files /dev/null and b/app/assets/images/emoji/raising_hand.png differ
diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c90e3e2689d72d568f3f78b53c69ff5167baa63
Binary files /dev/null and b/app/assets/images/emoji/raising_hand_tone1.png differ
diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..82c3ef2bfc5a4d5e668c29169cbc7becf55d6ad2
Binary files /dev/null and b/app/assets/images/emoji/raising_hand_tone2.png differ
diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b1da2aa0cac5477c4a0b710709ed4432c642555
Binary files /dev/null and b/app/assets/images/emoji/raising_hand_tone3.png differ
diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e453855c01ffad3f5d901c5ae0be3375e6b3a038
Binary files /dev/null and b/app/assets/images/emoji/raising_hand_tone4.png differ
diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..b86200fd844e900477123cff4faf3a383aa096ab
Binary files /dev/null and b/app/assets/images/emoji/raising_hand_tone5.png differ
diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png
new file mode 100644
index 0000000000000000000000000000000000000000..52a44464c9bd78882dca0cd6695e9d576f27d206
Binary files /dev/null and b/app/assets/images/emoji/ram.png differ
diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1cb7cd7384205e604550fad82ade2eadda068e7
Binary files /dev/null and b/app/assets/images/emoji/ramen.png differ
diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png
new file mode 100644
index 0000000000000000000000000000000000000000..86219144f1061ac04292e912fdb028cd4bf907fe
Binary files /dev/null and b/app/assets/images/emoji/rat.png differ
diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..ada52830fce0cbfd91fc197960a7ae8c3ca1470a
Binary files /dev/null and b/app/assets/images/emoji/record_button.png differ
diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png
new file mode 100644
index 0000000000000000000000000000000000000000..9221f095c37d7bb86826c861da2f2233aac4d7c5
Binary files /dev/null and b/app/assets/images/emoji/recycle.png differ
diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3e6a774dea6310d9c4a9eb850bae15c555fb0fa
Binary files /dev/null and b/app/assets/images/emoji/red_car.png differ
diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bef930d92f1e16d7f8c11060e753c9d2088bdd1
Binary files /dev/null and b/app/assets/images/emoji/red_circle.png differ
diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png
new file mode 100644
index 0000000000000000000000000000000000000000..53ef9f2d4e69db02d60ba6d4fdaed980ac4aa861
Binary files /dev/null and b/app/assets/images/emoji/registered.png differ
diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9e53c03d45ea2de7b5be09679da18be5446fc3c
Binary files /dev/null and b/app/assets/images/emoji/relaxed.png differ
diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png
new file mode 100644
index 0000000000000000000000000000000000000000..715ad0bf53f70a626f7817dfa80c0025755a3921
Binary files /dev/null and b/app/assets/images/emoji/relieved.png differ
diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3988bbd094c00492106175a2fa800dd0c7d45c4a
Binary files /dev/null and b/app/assets/images/emoji/reminder_ribbon.png differ
diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png
new file mode 100644
index 0000000000000000000000000000000000000000..540ce4e0fbaf941397937ac2ac0c3f5d198ea982
Binary files /dev/null and b/app/assets/images/emoji/repeat.png differ
diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png
new file mode 100644
index 0000000000000000000000000000000000000000..9567e83337ff85f693437510b0d1101d4fa9e78c
Binary files /dev/null and b/app/assets/images/emoji/repeat_one.png differ
diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png
new file mode 100644
index 0000000000000000000000000000000000000000..9588e0f0ef7dab632cfb1bf4473395a49bf35669
Binary files /dev/null and b/app/assets/images/emoji/restroom.png differ
diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b9d1948f7361f609282578b6500b1536e85cf30
Binary files /dev/null and b/app/assets/images/emoji/revolving_hearts.png differ
diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png
new file mode 100644
index 0000000000000000000000000000000000000000..e22e2bd3da5ee121a223e47938bd483c00fcab58
Binary files /dev/null and b/app/assets/images/emoji/rewind.png differ
diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png
new file mode 100644
index 0000000000000000000000000000000000000000..12f4e0d9d9be75b13ba7a7424b15beb9a2105714
Binary files /dev/null and b/app/assets/images/emoji/rhino.png differ
diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f253c3d8c8b9b54d0e89df77a7e76d3f889122d
Binary files /dev/null and b/app/assets/images/emoji/ribbon.png differ
diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e3ac7956b1a1346ee537636ad37f4208ea02a8e
Binary files /dev/null and b/app/assets/images/emoji/rice.png differ
diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3d8ee25cb849ab6c739e3d470c03e8cdc003106
Binary files /dev/null and b/app/assets/images/emoji/rice_ball.png differ
diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png
new file mode 100644
index 0000000000000000000000000000000000000000..7fbd08e4ff9f7353f81e763df0ad1220cf606b09
Binary files /dev/null and b/app/assets/images/emoji/rice_cracker.png differ
diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a28426592a7d833d116621f78eb4edff1a1b6b8
Binary files /dev/null and b/app/assets/images/emoji/rice_scene.png differ
diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png
new file mode 100644
index 0000000000000000000000000000000000000000..754ed066d2cd536050be9fa4c25be59bf3be3fa9
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist.png differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..33ded2f61a6ff02912d09622145b767ab6f20db9
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist_tone1.png differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..88054e335c74a70f93f5727d2be0e5715d553300
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist_tone2.png differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..84b9f5da7f7484f54977952bfda0c80a8b2bf33f
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist_tone3.png differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e741cfea68b2f841505742bfc80cc63cb9aa6b12
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist_tone4.png differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf66d760c1f7ccbe004a869d8c044a2c6a9cd9e6
Binary files /dev/null and b/app/assets/images/emoji/right_facing_fist_tone5.png differ
diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png
new file mode 100644
index 0000000000000000000000000000000000000000..87d227adb745c0079be065cbd3ba89ff04a04683
Binary files /dev/null and b/app/assets/images/emoji/ring.png differ
diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cc62612c6a80adf9c5e5cf2cb10d629184651a1
Binary files /dev/null and b/app/assets/images/emoji/robot.png differ
diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d8da089a37ee93797fe6ec131b240e9b4928b14
Binary files /dev/null and b/app/assets/images/emoji/rocket.png differ
diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1736fedfeb0a226e77d3388e8b68b8767a48c8c
Binary files /dev/null and b/app/assets/images/emoji/rofl.png differ
diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b849e071e83611dc128e754a3585f35b192f8bb
Binary files /dev/null and b/app/assets/images/emoji/roller_coaster.png differ
diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f77b9fc3b97df3022b55aa78a0c70b9ed9fd6c3
Binary files /dev/null and b/app/assets/images/emoji/rolling_eyes.png differ
diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png
new file mode 100644
index 0000000000000000000000000000000000000000..bbf2bbff97a3bdfd4679d023c61dffbdbd5b64aa
Binary files /dev/null and b/app/assets/images/emoji/rooster.png differ
diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png
new file mode 100644
index 0000000000000000000000000000000000000000..52c286d31ce22fa1a83a45113953d358aa50eab3
Binary files /dev/null and b/app/assets/images/emoji/rose.png differ
diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png
new file mode 100644
index 0000000000000000000000000000000000000000..8030e494bcfb1555a1eea10545b6fd7d64a0ee95
Binary files /dev/null and b/app/assets/images/emoji/rosette.png differ
diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png
new file mode 100644
index 0000000000000000000000000000000000000000..cad66b0afefb02a5629b3009729e0722692f2e7a
Binary files /dev/null and b/app/assets/images/emoji/rotating_light.png differ
diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png
new file mode 100644
index 0000000000000000000000000000000000000000..28b9d72866e286c9eb4cd60c8f4c51b5946ed5d5
Binary files /dev/null and b/app/assets/images/emoji/round_pushpin.png differ
diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd4dfc095d9a280764313a61c1217c8c24e4be6b
Binary files /dev/null and b/app/assets/images/emoji/rowboat.png differ
diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e5d18548cb0c4a0d9096f960dd6121a4a5db78e
Binary files /dev/null and b/app/assets/images/emoji/rowboat_tone1.png differ
diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b123ef88716d9ef49e086ed4f3bd39e6e5a4c1f
Binary files /dev/null and b/app/assets/images/emoji/rowboat_tone2.png differ
diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ebd89a55f508461ca69fab77f149c297e466013
Binary files /dev/null and b/app/assets/images/emoji/rowboat_tone3.png differ
diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b0d04f87255ea54b431031fe5acfe78afd29e72
Binary files /dev/null and b/app/assets/images/emoji/rowboat_tone4.png differ
diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..b346f2dfc84be9a4d451d8b141652fac09de5ad7
Binary files /dev/null and b/app/assets/images/emoji/rowboat_tone5.png differ
diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png
new file mode 100644
index 0000000000000000000000000000000000000000..b18722734368354533c5480b6fffa2b5cb633fd5
Binary files /dev/null and b/app/assets/images/emoji/rugby_football.png differ
diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png
new file mode 100644
index 0000000000000000000000000000000000000000..e914915976acc07af358c1161b05400bc56147ba
Binary files /dev/null and b/app/assets/images/emoji/runner.png differ
diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..9355239a52d2c925c5b994c2552b20f9f1e313c8
Binary files /dev/null and b/app/assets/images/emoji/runner_tone1.png differ
diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6112fd5c376bce535a0fd51a680f8be8c5e104a6
Binary files /dev/null and b/app/assets/images/emoji/runner_tone2.png differ
diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..625ec708f480b00d4b059cf961f501a504bec306
Binary files /dev/null and b/app/assets/images/emoji/runner_tone3.png differ
diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..242f1b56337344e248fe278876bd8528b6fc6f8f
Binary files /dev/null and b/app/assets/images/emoji/runner_tone4.png differ
diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..2976c6f019f9abe47baac95166dea48386c4f802
Binary files /dev/null and b/app/assets/images/emoji/runner_tone5.png differ
diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d83c06b803bdce7bcc49b49c2036dca30020a81
Binary files /dev/null and b/app/assets/images/emoji/running_shirt_with_sash.png differ
diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png
new file mode 100644
index 0000000000000000000000000000000000000000..900f9633247b6c7bd3da29f7939b2830383f722a
Binary files /dev/null and b/app/assets/images/emoji/sa.png differ
diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8d94ff292350d8d3d4a2da463f654a73e4036f6
Binary files /dev/null and b/app/assets/images/emoji/sagittarius.png differ
diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png
new file mode 100644
index 0000000000000000000000000000000000000000..772ef11da5d6d31c66aa2ec73e42e05a5c06ee82
Binary files /dev/null and b/app/assets/images/emoji/sailboat.png differ
diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png
new file mode 100644
index 0000000000000000000000000000000000000000..2933f5672c4010610a39f28ddabca5b654843a13
Binary files /dev/null and b/app/assets/images/emoji/sake.png differ
diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png
new file mode 100644
index 0000000000000000000000000000000000000000..c89f93411580e81758ac4f0d4cba71bf02a4eced
Binary files /dev/null and b/app/assets/images/emoji/salad.png differ
diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d9f5122b7aeb048cbac6a468fb09e4cb0e8aea4
Binary files /dev/null and b/app/assets/images/emoji/sandal.png differ
diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc83ab80d52d597f85b179425956129f2d2b1613
Binary files /dev/null and b/app/assets/images/emoji/santa.png differ
diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5233ffb71740310d2783e14ef25649d00810b617
Binary files /dev/null and b/app/assets/images/emoji/santa_tone1.png differ
diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e845438197729d1ef78b88c596ef48a230b91e4
Binary files /dev/null and b/app/assets/images/emoji/santa_tone2.png differ
diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..7fc4f33b60f7b8d0a41fb2e2c3b1980e654c4dff
Binary files /dev/null and b/app/assets/images/emoji/santa_tone3.png differ
diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1d5a15132ded0327dad9a64f72be359e44d2957
Binary files /dev/null and b/app/assets/images/emoji/santa_tone4.png differ
diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d697a01f24c347bb48813a7529a31145c57b103
Binary files /dev/null and b/app/assets/images/emoji/santa_tone5.png differ
diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png
new file mode 100644
index 0000000000000000000000000000000000000000..db0372795f4c0762e2b5e5ba53be5824fb1c0979
Binary files /dev/null and b/app/assets/images/emoji/satellite.png differ
diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ba55d6e297bbe3071f4df192c4a71c9101a9504
Binary files /dev/null and b/app/assets/images/emoji/satellite_orbital.png differ
diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png
new file mode 100644
index 0000000000000000000000000000000000000000..a392faec291741626e67eeaf5d2e014587a907a5
Binary files /dev/null and b/app/assets/images/emoji/saxophone.png differ
diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png
new file mode 100644
index 0000000000000000000000000000000000000000..0757eda1684cf934cd96962023cbfa57f842c500
Binary files /dev/null and b/app/assets/images/emoji/scales.png differ
diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png
new file mode 100644
index 0000000000000000000000000000000000000000..269759534f0ef8ecbebeb7b005c7e6318c7f2d5c
Binary files /dev/null and b/app/assets/images/emoji/school.png differ
diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png
new file mode 100644
index 0000000000000000000000000000000000000000..9997c86e7dc8187b7e10ef70ad4544615534641f
Binary files /dev/null and b/app/assets/images/emoji/school_satchel.png differ
diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png
new file mode 100644
index 0000000000000000000000000000000000000000..270571c8cddb04d331b08709c7618884ed5c1f61
Binary files /dev/null and b/app/assets/images/emoji/scissors.png differ
diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ab7ef59cd234e79b78b988c68dba619341ecabd
Binary files /dev/null and b/app/assets/images/emoji/scooter.png differ
diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png
new file mode 100644
index 0000000000000000000000000000000000000000..449a6b281c993298ef97b1d8ad9b4b4026bcf630
Binary files /dev/null and b/app/assets/images/emoji/scorpion.png differ
diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png
new file mode 100644
index 0000000000000000000000000000000000000000..c31a9920455e2e50fb0b368e0082353daec7513b
Binary files /dev/null and b/app/assets/images/emoji/scorpius.png differ
diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3bea9f25107ca855af161e851cfe8706b582f25
Binary files /dev/null and b/app/assets/images/emoji/scream.png differ
diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..15803ad8e6e7eb92d7a22f9333b9714dc6969330
Binary files /dev/null and b/app/assets/images/emoji/scream_cat.png differ
diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png
new file mode 100644
index 0000000000000000000000000000000000000000..50ee5dcd4b9e2dfd31e76dc3e7c7fe1a683d6d43
Binary files /dev/null and b/app/assets/images/emoji/scroll.png differ
diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6d72d95adb686a646a5d481d127d0a873ddbd64
Binary files /dev/null and b/app/assets/images/emoji/seat.png differ
diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png
new file mode 100644
index 0000000000000000000000000000000000000000..17b011268b6df4c524f586f46c626c120a8ac556
Binary files /dev/null and b/app/assets/images/emoji/second_place.png differ
diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fd72608e60ef5a9495462af0f68ff925b60e527
Binary files /dev/null and b/app/assets/images/emoji/secret.png differ
diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png
new file mode 100644
index 0000000000000000000000000000000000000000..5187e47453180bbdf6055c79c2b024c7f42d9b10
Binary files /dev/null and b/app/assets/images/emoji/see_no_evil.png differ
diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae0948bcfd6cf0c7539fb9d348d8da8616a419d7
Binary files /dev/null and b/app/assets/images/emoji/seedling.png differ
diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a1ba75c7e3911c11f29a5419b9a8ff756e2ef41
Binary files /dev/null and b/app/assets/images/emoji/selfie.png differ
diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..290e075b56fb29a81e1cb5494ec723d94352c815
Binary files /dev/null and b/app/assets/images/emoji/selfie_tone1.png differ
diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..fcd9595b64377838005067e24f211480969b2cdd
Binary files /dev/null and b/app/assets/images/emoji/selfie_tone2.png differ
diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3a22fdf4354afb4d3c50744840843b5fee0743e
Binary files /dev/null and b/app/assets/images/emoji/selfie_tone3.png differ
diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdecf6d9f4e50062ce3f987f17290fb0c3cb34a0
Binary files /dev/null and b/app/assets/images/emoji/selfie_tone4.png differ
diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..86acbb6c202cdca4ab9dbfbd7cedaca0b0b31be4
Binary files /dev/null and b/app/assets/images/emoji/selfie_tone5.png differ
diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b3476ae7c7b0603620a3bfbc72d4b9df1d05ebe
Binary files /dev/null and b/app/assets/images/emoji/seven.png differ
diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png
new file mode 100644
index 0000000000000000000000000000000000000000..663a1006acd7bb23f1c37624fa4a12757c7a2038
Binary files /dev/null and b/app/assets/images/emoji/shallow_pan_of_food.png differ
diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png
new file mode 100644
index 0000000000000000000000000000000000000000..f202aecfe6f7f55463cacc3ce65af48e082f758b
Binary files /dev/null and b/app/assets/images/emoji/shamrock.png differ
diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png
new file mode 100644
index 0000000000000000000000000000000000000000..c75076d57d8200c9dc5866fb3acd311122e4a1e9
Binary files /dev/null and b/app/assets/images/emoji/shark.png differ
diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png
new file mode 100644
index 0000000000000000000000000000000000000000..36dfb53ca93f7dff4b80e714ce099f0db85198a2
Binary files /dev/null and b/app/assets/images/emoji/shaved_ice.png differ
diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png
new file mode 100644
index 0000000000000000000000000000000000000000..102b8a52b28409fc78074506deabf04e5107bd30
Binary files /dev/null and b/app/assets/images/emoji/sheep.png differ
diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png
new file mode 100644
index 0000000000000000000000000000000000000000..55721629f626f729f72d98068c68d2f0e6591d1d
Binary files /dev/null and b/app/assets/images/emoji/shell.png differ
diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png
new file mode 100644
index 0000000000000000000000000000000000000000..610bf033ce0f8e58c81d47d6c66b2d6fe9085f64
Binary files /dev/null and b/app/assets/images/emoji/shield.png differ
diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a344975bf3842bdea4e312480049846ea741fa9
Binary files /dev/null and b/app/assets/images/emoji/shinto_shrine.png differ
diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png
new file mode 100644
index 0000000000000000000000000000000000000000..62d54f7d6c92b5ae141f9a11682adcc787c26694
Binary files /dev/null and b/app/assets/images/emoji/ship.png differ
diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png
new file mode 100644
index 0000000000000000000000000000000000000000..af08dec8b59c9cb9808af2149fdc542bd75db2a4
Binary files /dev/null and b/app/assets/images/emoji/shirt.png differ
diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png
new file mode 100644
index 0000000000000000000000000000000000000000..99f2a2b13acaac18948ecdc12031bc5fef0584fa
Binary files /dev/null and b/app/assets/images/emoji/shopping_bags.png differ
diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png
new file mode 100644
index 0000000000000000000000000000000000000000..1086fe6e45651b99684c15686c10dbdb189ae3d8
Binary files /dev/null and b/app/assets/images/emoji/shopping_cart.png differ
diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png
new file mode 100644
index 0000000000000000000000000000000000000000..156776a2e52014c3bb10609dae88b9750490214d
Binary files /dev/null and b/app/assets/images/emoji/shower.png differ
diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png
new file mode 100644
index 0000000000000000000000000000000000000000..49eff28a71e8ba95dc60b1186bac1857c509858c
Binary files /dev/null and b/app/assets/images/emoji/shrimp.png differ
diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png
new file mode 100644
index 0000000000000000000000000000000000000000..76e63bfac775fc3ae867c7d0a9f3ab0b738e18d6
Binary files /dev/null and b/app/assets/images/emoji/shrug.png differ
diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c895e64468baf529487656e49fe865f64c6366b
Binary files /dev/null and b/app/assets/images/emoji/shrug_tone1.png differ
diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e3ca8f8bac46d390cf19f0bf5603a83d71b4698
Binary files /dev/null and b/app/assets/images/emoji/shrug_tone2.png differ
diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1b16a19bb564079bca539c69b0ea3f1fa28d515
Binary files /dev/null and b/app/assets/images/emoji/shrug_tone3.png differ
diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fbef3f2255f172e0c31e3637bbf20f2e4e1b5e8
Binary files /dev/null and b/app/assets/images/emoji/shrug_tone4.png differ
diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4af2e28bc5cf2b6ff082d587913e7187e184e8c3
Binary files /dev/null and b/app/assets/images/emoji/shrug_tone5.png differ
diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee2b5a4b5193b3bd9948b20a2510d8dfe0a83f1c
Binary files /dev/null and b/app/assets/images/emoji/signal_strength.png differ
diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png
new file mode 100644
index 0000000000000000000000000000000000000000..371b3acef2cf72c99a912ca5e4eb7c1d56a4c777
Binary files /dev/null and b/app/assets/images/emoji/six.png differ
diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png
new file mode 100644
index 0000000000000000000000000000000000000000..2eb1707458ba2c823249c691a7f28f0324ea01b9
Binary files /dev/null and b/app/assets/images/emoji/six_pointed_star.png differ
diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a2d2c123066e824a56b338ebd7d32c1cb5d6bee
Binary files /dev/null and b/app/assets/images/emoji/ski.png differ
diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png
new file mode 100644
index 0000000000000000000000000000000000000000..2eb3bdce2afcaa27aad6b3452f3d7be79b1b9cc1
Binary files /dev/null and b/app/assets/images/emoji/skier.png differ
diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png
new file mode 100644
index 0000000000000000000000000000000000000000..26abb17296a21de36fafa6ba0d79b08436528a9f
Binary files /dev/null and b/app/assets/images/emoji/skull.png differ
diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png
new file mode 100644
index 0000000000000000000000000000000000000000..b459df9227adf391d14f30410cdf79f94c62abe1
Binary files /dev/null and b/app/assets/images/emoji/skull_crossbones.png differ
diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ecf600d6d832926098b93680323925226227672
Binary files /dev/null and b/app/assets/images/emoji/sleeping.png differ
diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png
new file mode 100644
index 0000000000000000000000000000000000000000..c739e7fb69bc32b7ad8ae03bf7444f47008ce0da
Binary files /dev/null and b/app/assets/images/emoji/sleeping_accommodation.png differ
diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png
new file mode 100644
index 0000000000000000000000000000000000000000..836b41077173be7d74dc0eafef53ce4a70f2dce0
Binary files /dev/null and b/app/assets/images/emoji/sleepy.png differ
diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2f1d983d363ad97e75d3d1b3deb60e1e629c6c8
Binary files /dev/null and b/app/assets/images/emoji/slight_frown.png differ
diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png
new file mode 100644
index 0000000000000000000000000000000000000000..ddd7d65dd3dbea8a67ca3359117a42c5c8fc528a
Binary files /dev/null and b/app/assets/images/emoji/slight_smile.png differ
diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee71b6c268c605b6d954f615079bc53124a1ed38
Binary files /dev/null and b/app/assets/images/emoji/slot_machine.png differ
diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png
new file mode 100644
index 0000000000000000000000000000000000000000..b86b5bc4db3f6aff7c680c46d50ca20d590bc81c
Binary files /dev/null and b/app/assets/images/emoji/small_blue_diamond.png differ
diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1c6ed9b2f8f7c13230ab00a99802e05e0279217
Binary files /dev/null and b/app/assets/images/emoji/small_orange_diamond.png differ
diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png
new file mode 100644
index 0000000000000000000000000000000000000000..785887c195a1089d36f1db4516a303da1d705ba2
Binary files /dev/null and b/app/assets/images/emoji/small_red_triangle.png differ
diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..a83beff1914e3c017a1482c27e0886874193d038
Binary files /dev/null and b/app/assets/images/emoji/small_red_triangle_down.png differ
diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa47ffe978cf408b68331ec442e3d786e39b3530
Binary files /dev/null and b/app/assets/images/emoji/smile.png differ
diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f25f11dd3a01822a31168c4511acb5016ad6d3b
Binary files /dev/null and b/app/assets/images/emoji/smile_cat.png differ
diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png
new file mode 100644
index 0000000000000000000000000000000000000000..30957a65968a46712c32f2c3c9512627b59b355b
Binary files /dev/null and b/app/assets/images/emoji/smiley.png differ
diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..163b57a3427903c8e97b8496a9d529bc64c4c4c9
Binary files /dev/null and b/app/assets/images/emoji/smiley_cat.png differ
diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc2c5f1ec72a531d123b718f1ffc6381c4d8bea6
Binary files /dev/null and b/app/assets/images/emoji/smiling_imp.png differ
diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png
new file mode 100644
index 0000000000000000000000000000000000000000..87852109988ca9b14373c617a94f05e8254e2315
Binary files /dev/null and b/app/assets/images/emoji/smirk.png differ
diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ac5954c199b5ef74846ed509845a4bc75e1a751
Binary files /dev/null and b/app/assets/images/emoji/smirk_cat.png differ
diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png
new file mode 100644
index 0000000000000000000000000000000000000000..910f648c8f9f2c9d86d14c3dc104b9e6913845d4
Binary files /dev/null and b/app/assets/images/emoji/smoking.png differ
diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4ea071e2d30deb0e8b4eb14d8b94c30ac00fc05
Binary files /dev/null and b/app/assets/images/emoji/snail.png differ
diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0278a28d8c83297ce21dfaa5986405fa72e1cba
Binary files /dev/null and b/app/assets/images/emoji/snake.png differ
diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..ccf07d4b64d5c6275643db8e288c0644bf590c53
Binary files /dev/null and b/app/assets/images/emoji/sneezing_face.png differ
diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png
new file mode 100644
index 0000000000000000000000000000000000000000..6361c0f2c9d1a5463f707cf9c2bc1242173d2c7f
Binary files /dev/null and b/app/assets/images/emoji/snowboarder.png differ
diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png
new file mode 100644
index 0000000000000000000000000000000000000000..db319a77ec6821240478f28516e2ad74f6ffa7dc
Binary files /dev/null and b/app/assets/images/emoji/snowflake.png differ
diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png
new file mode 100644
index 0000000000000000000000000000000000000000..20c177c2aff1c04ed780bda8f8c14ec16e04837a
Binary files /dev/null and b/app/assets/images/emoji/snowman.png differ
diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png
new file mode 100644
index 0000000000000000000000000000000000000000..896f28502af71277057861703db170bdcbb29fb3
Binary files /dev/null and b/app/assets/images/emoji/snowman2.png differ
diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png
new file mode 100644
index 0000000000000000000000000000000000000000..52e3517a1ee5b8ec4f7750dc22aeea936f16aab2
Binary files /dev/null and b/app/assets/images/emoji/sob.png differ
diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png
new file mode 100644
index 0000000000000000000000000000000000000000..28cfa218d6d49d2169cb5e75498f0c8d0208e706
Binary files /dev/null and b/app/assets/images/emoji/soccer.png differ
diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png
new file mode 100644
index 0000000000000000000000000000000000000000..8cdfd86690d764c85e7e84cfcc81c06e534f1d9d
Binary files /dev/null and b/app/assets/images/emoji/soon.png differ
diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7d8c9953e40060907b218eeae3351678a95494e
Binary files /dev/null and b/app/assets/images/emoji/sos.png differ
diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png
new file mode 100644
index 0000000000000000000000000000000000000000..e75ddca53ba13ba6789a93c285b2f792245254aa
Binary files /dev/null and b/app/assets/images/emoji/sound.png differ
diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e73f5f32e5918928e52e6bc1dfa1bb04956de22
Binary files /dev/null and b/app/assets/images/emoji/space_invader.png differ
diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png
new file mode 100644
index 0000000000000000000000000000000000000000..f822f184cb0efa34e56793050cfcd1bd650ee0a3
Binary files /dev/null and b/app/assets/images/emoji/spades.png differ
diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png
new file mode 100644
index 0000000000000000000000000000000000000000..89c24a321f17696635cd3b3fad073f7b3267ec21
Binary files /dev/null and b/app/assets/images/emoji/spaghetti.png differ
diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png
new file mode 100644
index 0000000000000000000000000000000000000000..6aa7b6ec9cf4bd7b88c0f1d3df7a8068520ad54e
Binary files /dev/null and b/app/assets/images/emoji/sparkle.png differ
diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png
new file mode 100644
index 0000000000000000000000000000000000000000..30339cd6e09ebded30a82d43a1646e3ae60026be
Binary files /dev/null and b/app/assets/images/emoji/sparkler.png differ
diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png
new file mode 100644
index 0000000000000000000000000000000000000000..169bc10b02301e99a3449983f440b9d3bd4b18f7
Binary files /dev/null and b/app/assets/images/emoji/sparkles.png differ
diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..6709269454ea3e40f39743e35c64081531c3b0ed
Binary files /dev/null and b/app/assets/images/emoji/sparkling_heart.png differ
diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d9e07c974b377572d5ffae523672dadc7032ec9
Binary files /dev/null and b/app/assets/images/emoji/speak_no_evil.png differ
diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png
new file mode 100644
index 0000000000000000000000000000000000000000..7bcffb8fc43741b145e08e83a3d66fec2f578a62
Binary files /dev/null and b/app/assets/images/emoji/speaker.png differ
diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png
new file mode 100644
index 0000000000000000000000000000000000000000..2df93aaae09bfa3845fe86c04ec7a60b6da931f2
Binary files /dev/null and b/app/assets/images/emoji/speaking_head.png differ
diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png
new file mode 100644
index 0000000000000000000000000000000000000000..a34ef741733a03186eb11ed7aefb8954739c268d
Binary files /dev/null and b/app/assets/images/emoji/speech_balloon.png differ
diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png
new file mode 100644
index 0000000000000000000000000000000000000000..74059d12de189821aea0461c8cf089b74ad186d1
Binary files /dev/null and b/app/assets/images/emoji/speedboat.png differ
diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png
new file mode 100644
index 0000000000000000000000000000000000000000..3849fa90b9495fd8bf41d72b25ffabf214118a6b
Binary files /dev/null and b/app/assets/images/emoji/spider.png differ
diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba448ee7fba5c2c4b6d0699604032aa572fd57ca
Binary files /dev/null and b/app/assets/images/emoji/spider_web.png differ
diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c4da766aee70e2a42b2f5005c050d8c655750d4
Binary files /dev/null and b/app/assets/images/emoji/spoon.png differ
diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png
new file mode 100644
index 0000000000000000000000000000000000000000..a729e9584d6d7d0b167576d3eec4f4212e66e0c7
Binary files /dev/null and b/app/assets/images/emoji/spy.png differ
diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2d1c022caeebad359331cb3b3d435c7957c34b3a
Binary files /dev/null and b/app/assets/images/emoji/spy_tone1.png differ
diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..548b9c26f5d93026eca05968d5a84b03b2918d48
Binary files /dev/null and b/app/assets/images/emoji/spy_tone2.png differ
diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b023f4b18e184e514bee1f77762f4e0473989b0c
Binary files /dev/null and b/app/assets/images/emoji/spy_tone3.png differ
diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8300af492db94cf77ff59dcb73557280c8db69e
Binary files /dev/null and b/app/assets/images/emoji/spy_tone4.png differ
diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca1462595faf76e11f5db3f86f6121bb984c18e2
Binary files /dev/null and b/app/assets/images/emoji/spy_tone5.png differ
diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2af223f0cb3a797b315b483884df99f7b973526
Binary files /dev/null and b/app/assets/images/emoji/squid.png differ
diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png
new file mode 100644
index 0000000000000000000000000000000000000000..00cd6db5e293653f305ed3e6b62515f5ae87777c
Binary files /dev/null and b/app/assets/images/emoji/stadium.png differ
diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..c930947076ebe4feb6a6701cdf913e9007cdd10b
Binary files /dev/null and b/app/assets/images/emoji/star.png differ
diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f5cba592db0c20cc10e7166c4952930eaa22f46
Binary files /dev/null and b/app/assets/images/emoji/star2.png differ
diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png
new file mode 100644
index 0000000000000000000000000000000000000000..e182636457d3c09ec6d174e2525d0080cf839e37
Binary files /dev/null and b/app/assets/images/emoji/star_and_crescent.png differ
diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc59d0dde2408eb938e19a55a338f1bb9290e150
Binary files /dev/null and b/app/assets/images/emoji/star_of_david.png differ
diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa45384d1c6670110f621caadb672fd29a9a353f
Binary files /dev/null and b/app/assets/images/emoji/stars.png differ
diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c26fee529cc3d829f35c6c7216bd8560cbaf132
Binary files /dev/null and b/app/assets/images/emoji/station.png differ
diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png
new file mode 100644
index 0000000000000000000000000000000000000000..05df8289b5905a9dea29d2ca2771509e35757334
Binary files /dev/null and b/app/assets/images/emoji/statue_of_liberty.png differ
diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ac0d999c4c6d0a77cc5f57ffb35580ebd368b7f
Binary files /dev/null and b/app/assets/images/emoji/steam_locomotive.png differ
diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b3f010c17a325d364dc88ce8aa5c47a4f5a7adc
Binary files /dev/null and b/app/assets/images/emoji/stew.png differ
diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..cfa99988ac2cd8ce611eebb4dc830f728175816e
Binary files /dev/null and b/app/assets/images/emoji/stop_button.png differ
diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fae1c9a8981f24f3f5756d0c096c2bde60729de
Binary files /dev/null and b/app/assets/images/emoji/stopwatch.png differ
diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png
new file mode 100644
index 0000000000000000000000000000000000000000..1017b7433a1d9541b40aa3073acfdec11715db67
Binary files /dev/null and b/app/assets/images/emoji/straight_ruler.png differ
diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png
new file mode 100644
index 0000000000000000000000000000000000000000..7bb86f0b29cd557dd2196831cdebeb1eb64d68ad
Binary files /dev/null and b/app/assets/images/emoji/strawberry.png differ
diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png
new file mode 100644
index 0000000000000000000000000000000000000000..25757341f9604c7fecf535e8b45d4c3003d0d538
Binary files /dev/null and b/app/assets/images/emoji/stuck_out_tongue.png differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c0401e9b1d3d0f1ed2f6c978d83aa99df2e6f3f
Binary files /dev/null and b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
new file mode 100644
index 0000000000000000000000000000000000000000..4817eaa3dc67c0820bc5b59d1c5ce96b87cda517
Binary files /dev/null and b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png differ
diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2e10df40a5dc9f990a48c95bbbf9e909b99cf79
Binary files /dev/null and b/app/assets/images/emoji/stuffed_flatbread.png differ
diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..14a4ea971db2ee8eb6a81c779882ab8e54c5e1b3
Binary files /dev/null and b/app/assets/images/emoji/sun_with_face.png differ
diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png
new file mode 100644
index 0000000000000000000000000000000000000000..08cc07761ea7ba28b927ae44ced44dc586578035
Binary files /dev/null and b/app/assets/images/emoji/sunflower.png differ
diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png
new file mode 100644
index 0000000000000000000000000000000000000000..20011735110992abb08ce5937c3bf74aa1d22fc3
Binary files /dev/null and b/app/assets/images/emoji/sunglasses.png differ
diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd521ae31a73a54d1fb05b168633138a26aceaf1
Binary files /dev/null and b/app/assets/images/emoji/sunny.png differ
diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ad36003c204663ebf80538b81b497610ceac777
Binary files /dev/null and b/app/assets/images/emoji/sunrise.png differ
diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b99307344d5cfb9ac7bad20fe1e12603e3baae3
Binary files /dev/null and b/app/assets/images/emoji/sunrise_over_mountains.png differ
diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ab017adf4bc86c58e4ba9278b914953d79d2f2d
Binary files /dev/null and b/app/assets/images/emoji/surfer.png differ
diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5faaa524cceb0b7558a0223e6378b82106c5e02
Binary files /dev/null and b/app/assets/images/emoji/surfer_tone1.png differ
diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d92e412ff1d9b4890fa3ebd3af5683a1b36f5df
Binary files /dev/null and b/app/assets/images/emoji/surfer_tone2.png differ
diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..f05ef59496ed45a93fa7e9d171d14497c66371c2
Binary files /dev/null and b/app/assets/images/emoji/surfer_tone3.png differ
diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..35e143d19dcbcb71072b3c8392e046475cc96432
Binary files /dev/null and b/app/assets/images/emoji/surfer_tone4.png differ
diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..38917658eac479518db01ecda1a105fffb9e4d69
Binary files /dev/null and b/app/assets/images/emoji/surfer_tone5.png differ
diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png
new file mode 100644
index 0000000000000000000000000000000000000000..f171fd2f7a1d9cabf0cf0784d2279ae8ff2176a8
Binary files /dev/null and b/app/assets/images/emoji/sushi.png differ
diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png
new file mode 100644
index 0000000000000000000000000000000000000000..a59d5f48c246591fcb637dc10b9e22aa188e07e1
Binary files /dev/null and b/app/assets/images/emoji/suspension_railway.png differ
diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png
new file mode 100644
index 0000000000000000000000000000000000000000..f0dae7b78934463a1a877f61b6846df49e95d36c
Binary files /dev/null and b/app/assets/images/emoji/sweat.png differ
diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png
new file mode 100644
index 0000000000000000000000000000000000000000..4106117ebc85a922387fd0583208c7faf4e9401f
Binary files /dev/null and b/app/assets/images/emoji/sweat_drops.png differ
diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb18d9c899b8b9f8a39e8cc4402b7db429c3c085
Binary files /dev/null and b/app/assets/images/emoji/sweat_smile.png differ
diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png
new file mode 100644
index 0000000000000000000000000000000000000000..92a425f2e2095607102927bea204bdb4caa8ffc5
Binary files /dev/null and b/app/assets/images/emoji/sweet_potato.png differ
diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png
new file mode 100644
index 0000000000000000000000000000000000000000..55b4d72f9a7e390fd5581eef5584cb8ad9cb8243
Binary files /dev/null and b/app/assets/images/emoji/swimmer.png differ
diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..38441c9ca9a7cc674da418bcfb3def3855fc1aa3
Binary files /dev/null and b/app/assets/images/emoji/swimmer_tone1.png differ
diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0d4311244433ee715ee02b468339c018581bd08
Binary files /dev/null and b/app/assets/images/emoji/swimmer_tone2.png differ
diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..211e77e2aa00af4cd5787215b7dfb4f2cdc99ae4
Binary files /dev/null and b/app/assets/images/emoji/swimmer_tone3.png differ
diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..f34c34db9d24c3d980d67f03deb623b428362dda
Binary files /dev/null and b/app/assets/images/emoji/swimmer_tone4.png differ
diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e9231ff868158c93cb9d5efb5dc7ceae72e181c
Binary files /dev/null and b/app/assets/images/emoji/swimmer_tone5.png differ
diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac2fc1f358fab3f4427e93b9fb1f32fe3d2fd9a4
Binary files /dev/null and b/app/assets/images/emoji/symbols.png differ
diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee347904c80241c51de3917826a98a2c712ab69f
Binary files /dev/null and b/app/assets/images/emoji/synagogue.png differ
diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png
new file mode 100644
index 0000000000000000000000000000000000000000..71c1a9528d58533fd7a56c0c81612646f4681478
Binary files /dev/null and b/app/assets/images/emoji/syringe.png differ
diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png
new file mode 100644
index 0000000000000000000000000000000000000000..10e847a461914136ace29b4f6d9dd1c8c2ffe33f
Binary files /dev/null and b/app/assets/images/emoji/taco.png differ
diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png
new file mode 100644
index 0000000000000000000000000000000000000000..0244d60f269e1d7de68c553da3000c4ad05946ba
Binary files /dev/null and b/app/assets/images/emoji/tada.png differ
diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..46fcb3a1aac7de4c5a0a89f811b8cdd065df540f
Binary files /dev/null and b/app/assets/images/emoji/tanabata_tree.png differ
diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab14e5378dbba65946906b3c94fc0f80f07d86ff
Binary files /dev/null and b/app/assets/images/emoji/tangerine.png differ
diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2a370df42b7e8034fcb977f45c943b2b87d4197
Binary files /dev/null and b/app/assets/images/emoji/taurus.png differ
diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png
new file mode 100644
index 0000000000000000000000000000000000000000..55f4cc84797aee836b8b65bd533790912d7fab40
Binary files /dev/null and b/app/assets/images/emoji/taxi.png differ
diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png
new file mode 100644
index 0000000000000000000000000000000000000000..b53b98f0c45910cc3fdc61d8073b4eaa1a654777
Binary files /dev/null and b/app/assets/images/emoji/tea.png differ
diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1e69f566bcf556a7089c495d912aa0dc4a64fa2
Binary files /dev/null and b/app/assets/images/emoji/telephone.png differ
diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png
new file mode 100644
index 0000000000000000000000000000000000000000..69388316c355995e2c279eb9cda9dfdbd1402305
Binary files /dev/null and b/app/assets/images/emoji/telephone_receiver.png differ
diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png
new file mode 100644
index 0000000000000000000000000000000000000000..d63154614b5113b2adedfdbe4d61a60d3e3b6753
Binary files /dev/null and b/app/assets/images/emoji/telescope.png differ
diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png
new file mode 100644
index 0000000000000000000000000000000000000000..782d4004962b4aa818435b39c7acb027fd3fe9f7
Binary files /dev/null and b/app/assets/images/emoji/ten.png differ
diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e68ba8f3019161098d31d71296c1f907c4d972a
Binary files /dev/null and b/app/assets/images/emoji/tennis.png differ
diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png
new file mode 100644
index 0000000000000000000000000000000000000000..3fddcfc56eb1e60c06d28bcd69b8bca9a3acb50f
Binary files /dev/null and b/app/assets/images/emoji/tent.png differ
diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png
new file mode 100644
index 0000000000000000000000000000000000000000..b114739242661113d26451d1cdb0d1187fe96786
Binary files /dev/null and b/app/assets/images/emoji/thermometer.png differ
diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fc57387563fee24031c2095db1bf4f9d6443ec2
Binary files /dev/null and b/app/assets/images/emoji/thermometer_face.png differ
diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png
new file mode 100644
index 0000000000000000000000000000000000000000..c18f6fd14added44386dfc482b0920421b360332
Binary files /dev/null and b/app/assets/images/emoji/thinking.png differ
diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png
new file mode 100644
index 0000000000000000000000000000000000000000..636e04a59509121109164216eb20fbc32c2116bc
Binary files /dev/null and b/app/assets/images/emoji/third_place.png differ
diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png
new file mode 100644
index 0000000000000000000000000000000000000000..72fe8fa7022dc660bd681dfe2bdc0c6a4cc4240c
Binary files /dev/null and b/app/assets/images/emoji/thought_balloon.png differ
diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png
new file mode 100644
index 0000000000000000000000000000000000000000..dbaa6183e7286ca1b90a2b7576d0c015b83b174d
Binary files /dev/null and b/app/assets/images/emoji/three.png differ
diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..b63da2f20a8378786d90a9078bbf90e9b3486152
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown.png differ
diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1631af8e92ef395e9dfb9009ca560cbaf6a7784
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown_tone1.png differ
diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..85fff82d595729acecfb187228cac2a5020102be
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown_tone2.png differ
diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..eeba3be80fdb60aea3ead2d82b03895c30b68b32
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown_tone3.png differ
diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..1addafdaed0d9381b9bb4822f1e52b2221cdb906
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown_tone4.png differ
diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..37ec07b57214ac6d961d92fa93884a82024071cf
Binary files /dev/null and b/app/assets/images/emoji/thumbsdown_tone5.png differ
diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9e6f13a34f3b324b29f50de36447bd52af91605
Binary files /dev/null and b/app/assets/images/emoji/thumbsup.png differ
diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..39684cd5cc71c4f19daf42212f9e18319053b1ca
Binary files /dev/null and b/app/assets/images/emoji/thumbsup_tone1.png differ
diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9b597235739b37fc847912b4ffb87ea96ca3d19
Binary files /dev/null and b/app/assets/images/emoji/thumbsup_tone2.png differ
diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5e2916701560b817ffd4db1f1f5beac8b27cc39
Binary files /dev/null and b/app/assets/images/emoji/thumbsup_tone3.png differ
diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bf4857a8842364e514808ecb4e9db88f1a222dd
Binary files /dev/null and b/app/assets/images/emoji/thumbsup_tone4.png differ
diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..d829f787c61a19edcd19300b269ccb0fc7d5c220
Binary files /dev/null and b/app/assets/images/emoji/thumbsup_tone5.png differ
diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png
new file mode 100644
index 0000000000000000000000000000000000000000..31a26a1b6ee922bdea5d8b33c0eeb06a6f5334e5
Binary files /dev/null and b/app/assets/images/emoji/thunder_cloud_rain.png differ
diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png
new file mode 100644
index 0000000000000000000000000000000000000000..605936bb6b32f20684f06f564175b4d7e77c802e
Binary files /dev/null and b/app/assets/images/emoji/ticket.png differ
diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png
new file mode 100644
index 0000000000000000000000000000000000000000..e510f4a7a502dba2609226972f931e1b42e0e60f
Binary files /dev/null and b/app/assets/images/emoji/tickets.png differ
diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4d3ef086d46df6b819cf62a60ef16f4e7d3a721
Binary files /dev/null and b/app/assets/images/emoji/tiger.png differ
diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png
new file mode 100644
index 0000000000000000000000000000000000000000..871a8b74d563898b5ba11fda0a4c2ceefcbe8c1c
Binary files /dev/null and b/app/assets/images/emoji/tiger2.png differ
diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png
new file mode 100644
index 0000000000000000000000000000000000000000..8a3be574c24bd8faff16c1ca9b75ba698fa3a97c
Binary files /dev/null and b/app/assets/images/emoji/timer.png differ
diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e01eff5b234011a8e612f154670d6c9cbc98ed9
Binary files /dev/null and b/app/assets/images/emoji/tired_face.png differ
diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a0c44a2c2b4493d806f2a2e98c31c1eeab43379
Binary files /dev/null and b/app/assets/images/emoji/tm.png differ
diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png
new file mode 100644
index 0000000000000000000000000000000000000000..1392f761835450dbf04aab2176e2da728b8cc39f
Binary files /dev/null and b/app/assets/images/emoji/toilet.png differ
diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png
new file mode 100644
index 0000000000000000000000000000000000000000..37df7fc65b1703abd906e05dabef931a2ce1e66b
Binary files /dev/null and b/app/assets/images/emoji/tokyo_tower.png differ
diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png
new file mode 100644
index 0000000000000000000000000000000000000000..497da8f6b227c72f558a478ae86cdb13ff34ffbc
Binary files /dev/null and b/app/assets/images/emoji/tomato.png differ
diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c395f3d0d68a0cf4f0d7ba19183278f630008264
Binary files /dev/null and b/app/assets/images/emoji/tone1.png differ
diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..080847431c18f5749200660df3749f458af257a6
Binary files /dev/null and b/app/assets/images/emoji/tone2.png differ
diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..482dd4034750bd395772a256d18ef5dce5239b1d
Binary files /dev/null and b/app/assets/images/emoji/tone3.png differ
diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5cae8bb20b044b91f462e24c41eb793ee211ea7e
Binary files /dev/null and b/app/assets/images/emoji/tone4.png differ
diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..49d1a8c3a64bd6ecd1576d61972b4e78bfc578b7
Binary files /dev/null and b/app/assets/images/emoji/tone5.png differ
diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png
new file mode 100644
index 0000000000000000000000000000000000000000..70ce9c1225f8a00f00200da5b3cdd47158e44afc
Binary files /dev/null and b/app/assets/images/emoji/tongue.png differ
diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c6049273a9a8f046fc226b7bb4088a8a50ed5ae
Binary files /dev/null and b/app/assets/images/emoji/tools.png differ
diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png
new file mode 100644
index 0000000000000000000000000000000000000000..49dea8c08b57436f7475522d6b87d5b16820910e
Binary files /dev/null and b/app/assets/images/emoji/top.png differ
diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png
new file mode 100644
index 0000000000000000000000000000000000000000..131b657b1096112b3d35c70c39fe53c210f077b0
Binary files /dev/null and b/app/assets/images/emoji/tophat.png differ
diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8880d33babb2257b7de4f5c02d97a94b42b414c
Binary files /dev/null and b/app/assets/images/emoji/track_next.png differ
diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ffd0566cfcbd62ceb4ff184f6c50ac1006a32ac
Binary files /dev/null and b/app/assets/images/emoji/track_previous.png differ
diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png
new file mode 100644
index 0000000000000000000000000000000000000000..3bea84ad7ceeb7447379eee2736f014d5742375c
Binary files /dev/null and b/app/assets/images/emoji/trackball.png differ
diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1bf8cae44f5a0c991720b78ba804d11ff4ee280
Binary files /dev/null and b/app/assets/images/emoji/tractor.png differ
diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b312285b0065051228864da362abb41ac1e7eda
Binary files /dev/null and b/app/assets/images/emoji/traffic_light.png differ
diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c80321f7e8b2de89ce471363107dccc7bab530f
Binary files /dev/null and b/app/assets/images/emoji/train.png differ
diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png
new file mode 100644
index 0000000000000000000000000000000000000000..367c7bc5d3968edba45c593622bc020992585dab
Binary files /dev/null and b/app/assets/images/emoji/train2.png differ
diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6f0e69038fab3b77013b876575e94baf9c5f292
Binary files /dev/null and b/app/assets/images/emoji/tram.png differ
diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png
new file mode 100644
index 0000000000000000000000000000000000000000..c12d8b0688693f7476b61944e232b8454c9937fa
Binary files /dev/null and b/app/assets/images/emoji/triangular_flag_on_post.png differ
diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png
new file mode 100644
index 0000000000000000000000000000000000000000..77dee9ee8435df04da64fe01247a01968b413247
Binary files /dev/null and b/app/assets/images/emoji/triangular_ruler.png differ
diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png
new file mode 100644
index 0000000000000000000000000000000000000000..777a1dad1216439a48a8f997f284240d00376426
Binary files /dev/null and b/app/assets/images/emoji/trident.png differ
diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png
new file mode 100644
index 0000000000000000000000000000000000000000..0be7a5019696a9c60ab8d58b77a888b00556e15b
Binary files /dev/null and b/app/assets/images/emoji/triumph.png differ
diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png
new file mode 100644
index 0000000000000000000000000000000000000000..139a9931b529dee982fa19b99dca98253f159f09
Binary files /dev/null and b/app/assets/images/emoji/trolleybus.png differ
diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac2895c18965956f94fabf1756cf532e28368191
Binary files /dev/null and b/app/assets/images/emoji/trophy.png differ
diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd714f81b3662b1ddcd623105d1f2b4514e55c69
Binary files /dev/null and b/app/assets/images/emoji/tropical_drink.png differ
diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..252105235a640af4f66514a62c0520427aa514b7
Binary files /dev/null and b/app/assets/images/emoji/tropical_fish.png differ
diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png
new file mode 100644
index 0000000000000000000000000000000000000000..130de047f8b292ce7ccfe9f9b4327128a3bd8715
Binary files /dev/null and b/app/assets/images/emoji/truck.png differ
diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png
new file mode 100644
index 0000000000000000000000000000000000000000..864ccbcd04a66a5fba7d32e846465e127a00c9ce
Binary files /dev/null and b/app/assets/images/emoji/trumpet.png differ
diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png
new file mode 100644
index 0000000000000000000000000000000000000000..f799d75c1821ac7e73ebff63a4eec5db61b0d999
Binary files /dev/null and b/app/assets/images/emoji/tulip.png differ
diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png
new file mode 100644
index 0000000000000000000000000000000000000000..7bf09229879a0cc3ff2f072e6fe2b2dad3189f14
Binary files /dev/null and b/app/assets/images/emoji/tumbler_glass.png differ
diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png
new file mode 100644
index 0000000000000000000000000000000000000000..344af94c9ecd23d9e48ecb6736e6ce828960e348
Binary files /dev/null and b/app/assets/images/emoji/turkey.png differ
diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png
new file mode 100644
index 0000000000000000000000000000000000000000..c22f7519fe87f67e39e8344f2f1958b95ebfdace
Binary files /dev/null and b/app/assets/images/emoji/turtle.png differ
diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png
new file mode 100644
index 0000000000000000000000000000000000000000..999f1fb5c6dde0509425367a1a1c20ecfdc7ba6c
Binary files /dev/null and b/app/assets/images/emoji/tv.png differ
diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png
new file mode 100644
index 0000000000000000000000000000000000000000..5904badde65edeea86e47b1cd6316ae7668a5a19
Binary files /dev/null and b/app/assets/images/emoji/twisted_rightwards_arrows.png differ
diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png
new file mode 100644
index 0000000000000000000000000000000000000000..927339c9bffa43a23a70dcf7590a4988be86bec2
Binary files /dev/null and b/app/assets/images/emoji/two.png differ
diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d8c3386042e7fb6a12af66b85092cbcf16f6135
Binary files /dev/null and b/app/assets/images/emoji/two_hearts.png differ
diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png
new file mode 100644
index 0000000000000000000000000000000000000000..a511fda822ab10369c208f6975408c672e8b93c6
Binary files /dev/null and b/app/assets/images/emoji/two_men_holding_hands.png differ
diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png
new file mode 100644
index 0000000000000000000000000000000000000000..b077cd3e40f0ff5e7a818d04b4bbcac4ab207e0a
Binary files /dev/null and b/app/assets/images/emoji/two_women_holding_hands.png differ
diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4f837fe68451f673e83987c1c56b1119651b58c
Binary files /dev/null and b/app/assets/images/emoji/u5272.png differ
diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png
new file mode 100644
index 0000000000000000000000000000000000000000..8375ad9d9af80a3de7e75f4bbc19fd93f6199d25
Binary files /dev/null and b/app/assets/images/emoji/u5408.png differ
diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png
new file mode 100644
index 0000000000000000000000000000000000000000..d21cb30eaf319fac2f315dd38f36f929908f5d3a
Binary files /dev/null and b/app/assets/images/emoji/u55b6.png differ
diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png
new file mode 100644
index 0000000000000000000000000000000000000000..078e23e4ff349c999291e904a8438179c5bd340d
Binary files /dev/null and b/app/assets/images/emoji/u6307.png differ
diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png
new file mode 100644
index 0000000000000000000000000000000000000000..c41bd36a26a24f17318ab5c6934098aeadf8c69f
Binary files /dev/null and b/app/assets/images/emoji/u6708.png differ
diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4510de41c03d9adbc8d4b1d740093a692a049eb
Binary files /dev/null and b/app/assets/images/emoji/u6709.png differ
diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9dea8b8833fcdef7dbed340986611635b654156
Binary files /dev/null and b/app/assets/images/emoji/u6e80.png differ
diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3a19b420de541866c3158d5a19c9ce4505e5232
Binary files /dev/null and b/app/assets/images/emoji/u7121.png differ
diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b7af0ee22298848e5be86adc0107938fb289452
Binary files /dev/null and b/app/assets/images/emoji/u7533.png differ
diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png
new file mode 100644
index 0000000000000000000000000000000000000000..4c704e03433c3c7da9d0b7b0ef637e19f83f7d7e
Binary files /dev/null and b/app/assets/images/emoji/u7981.png differ
diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png
new file mode 100644
index 0000000000000000000000000000000000000000..47966c1ea935ae7c7cbfd3bb287b4b60389f4679
Binary files /dev/null and b/app/assets/images/emoji/u7a7a.png differ
diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b35b7ff6a4b5a4f43c8502a94695688da147d10
Binary files /dev/null and b/app/assets/images/emoji/umbrella.png differ
diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png
new file mode 100644
index 0000000000000000000000000000000000000000..97fe859e74ff691bbd18a37974264a0073dd7abe
Binary files /dev/null and b/app/assets/images/emoji/umbrella2.png differ
diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png
new file mode 100644
index 0000000000000000000000000000000000000000..25e3677f2ebe8176aaab5610c622741b0727d74e
Binary files /dev/null and b/app/assets/images/emoji/unamused.png differ
diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png
new file mode 100644
index 0000000000000000000000000000000000000000..6dfe6da51e20393a22719c22a4ebdb06b4aa33d4
Binary files /dev/null and b/app/assets/images/emoji/underage.png differ
diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png
new file mode 100644
index 0000000000000000000000000000000000000000..05a97969f7efe55932a7d33cfcda6159ecb4595b
Binary files /dev/null and b/app/assets/images/emoji/unicorn.png differ
diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a74a693911ecfb2a508c8cddfe1ddb604241071
Binary files /dev/null and b/app/assets/images/emoji/unlock.png differ
diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d42142ba049aff1c412a99673a7c3528d84bdab
Binary files /dev/null and b/app/assets/images/emoji/up.png differ
diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..128f31c9828655d0f2eb13a3c1285bcd4e671410
Binary files /dev/null and b/app/assets/images/emoji/upside_down.png differ
diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b5b3503438600666a68ef188771a0c1f06179b1
Binary files /dev/null and b/app/assets/images/emoji/urn.png differ
diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png
new file mode 100644
index 0000000000000000000000000000000000000000..70c5516ffeec76fbdd88b74c1575828d1dabb219
Binary files /dev/null and b/app/assets/images/emoji/v.png differ
diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ac54a745f40e677d94affcd409942e5166b4638
Binary files /dev/null and b/app/assets/images/emoji/v_tone1.png differ
diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6dd9669866d58f7a243e42c70072ddfa9bee0dcc
Binary files /dev/null and b/app/assets/images/emoji/v_tone2.png differ
diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a615e53f02ffac612b2b8f1df1a06fd791b883db
Binary files /dev/null and b/app/assets/images/emoji/v_tone3.png differ
diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..33a34bd5a786ae30a77d179d259c34c6190e7ae9
Binary files /dev/null and b/app/assets/images/emoji/v_tone4.png differ
diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..45ad14b6c9ccf2e4487b23ec46016b6e7e78f322
Binary files /dev/null and b/app/assets/images/emoji/v_tone5.png differ
diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png
new file mode 100644
index 0000000000000000000000000000000000000000..8085973eecf432b138534f9e2d050829c40462c6
Binary files /dev/null and b/app/assets/images/emoji/vertical_traffic_light.png differ
diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9eb78ecd92e99366cd86a6c539c3b1a6e7ba635
Binary files /dev/null and b/app/assets/images/emoji/vhs.png differ
diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc46510e48eeb20b85ddb5a30403c6472893b2db
Binary files /dev/null and b/app/assets/images/emoji/vibration_mode.png differ
diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..85b300d425c3d75127639f763a5199afbc18f6d4
Binary files /dev/null and b/app/assets/images/emoji/video_camera.png differ
diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png
new file mode 100644
index 0000000000000000000000000000000000000000..316a9106a55759f0997ac6f67ed1bf26e8c7c423
Binary files /dev/null and b/app/assets/images/emoji/video_game.png differ
diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1e76cce242409623e8caa2a3967073419cff3b6
Binary files /dev/null and b/app/assets/images/emoji/violin.png differ
diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6b56c2cb5e442a777c33ba486282aadea50c59d
Binary files /dev/null and b/app/assets/images/emoji/virgo.png differ
diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png
new file mode 100644
index 0000000000000000000000000000000000000000..931d569294c53b31482121130fad60f442465525
Binary files /dev/null and b/app/assets/images/emoji/volcano.png differ
diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a0e49d4b07450c18c4747f6882a446073adcea8
Binary files /dev/null and b/app/assets/images/emoji/volleyball.png differ
diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1180f4a464084502c6b564ebbf9c499012c5ef0
Binary files /dev/null and b/app/assets/images/emoji/vs.png differ
diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png
new file mode 100644
index 0000000000000000000000000000000000000000..54728bcaf5cd192b5304d2ebca92dcaff675f7ae
Binary files /dev/null and b/app/assets/images/emoji/vulcan.png differ
diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aff5d8fa16b57bc97b20990d835385a09e0fc69
Binary files /dev/null and b/app/assets/images/emoji/vulcan_tone1.png differ
diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..82b7ad519b42781694f02dc710148713d2b00c95
Binary files /dev/null and b/app/assets/images/emoji/vulcan_tone2.png differ
diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1400e1dd28f63d1b76cfbc5d69ab8c4f2cd4a26
Binary files /dev/null and b/app/assets/images/emoji/vulcan_tone3.png differ
diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..47e2b280148864b6149ce2230a3f7cf4b32bf3ea
Binary files /dev/null and b/app/assets/images/emoji/vulcan_tone4.png differ
diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..60b5c6077be48cfb5748a9c9b10ef305dd76bb72
Binary files /dev/null and b/app/assets/images/emoji/vulcan_tone5.png differ
diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png
new file mode 100644
index 0000000000000000000000000000000000000000..06dc169a3fd58eec42129a5c60fa61a03d95ce74
Binary files /dev/null and b/app/assets/images/emoji/walking.png differ
diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e391b45a0b2afe0ccb59e45fbbecff1b48df81c
Binary files /dev/null and b/app/assets/images/emoji/walking_tone1.png differ
diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..31f94a1bce119b347de613967a4de93feb21a105
Binary files /dev/null and b/app/assets/images/emoji/walking_tone2.png differ
diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7ed8e39c2ea69733f1790164a00dc579287cfaf
Binary files /dev/null and b/app/assets/images/emoji/walking_tone3.png differ
diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..e58dc04c7b21d74ee5633f5818595a36880582ea
Binary files /dev/null and b/app/assets/images/emoji/walking_tone4.png differ
diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba4e1b58fcb8988ac515b7bb1cc2ad9782218181
Binary files /dev/null and b/app/assets/images/emoji/walking_tone5.png differ
diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf68706b871d57d94c71f675b28892c58438e537
Binary files /dev/null and b/app/assets/images/emoji/waning_crescent_moon.png differ
diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..24e16266119230df0d1963528f202e47e8891996
Binary files /dev/null and b/app/assets/images/emoji/waning_gibbous_moon.png differ
diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png
new file mode 100644
index 0000000000000000000000000000000000000000..35691c2ed9710c09787dd2cc102e768d8f6b6219
Binary files /dev/null and b/app/assets/images/emoji/warning.png differ
diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b3c484b498792902a48cb4ebdd445312c6f8d7e
Binary files /dev/null and b/app/assets/images/emoji/wastebasket.png differ
diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png
new file mode 100644
index 0000000000000000000000000000000000000000..64819bc6e21444ccc4c474ab3da594401457ca23
Binary files /dev/null and b/app/assets/images/emoji/watch.png differ
diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png
new file mode 100644
index 0000000000000000000000000000000000000000..80446615cafc124b090116fd48ab16e1e63463cc
Binary files /dev/null and b/app/assets/images/emoji/water_buffalo.png differ
diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb44576780d41affe31b56ecd3d76f23a9c56809
Binary files /dev/null and b/app/assets/images/emoji/water_polo.png differ
diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..bed1a908d6aeda8ec26f2fb92e5f425b95f6f92c
Binary files /dev/null and b/app/assets/images/emoji/water_polo_tone1.png differ
diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec5a43b4d4a76bb86d58471d6424c16193616135
Binary files /dev/null and b/app/assets/images/emoji/water_polo_tone2.png differ
diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b081a4a5a965da0ff0fe7ecb4ae7add21bcf1280
Binary files /dev/null and b/app/assets/images/emoji/water_polo_tone3.png differ
diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..82cfbc3b0c7c79cc574bc90f499f3ecc52f61366
Binary files /dev/null and b/app/assets/images/emoji/water_polo_tone4.png differ
diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd3366eb06c3d1d7147b72845f2a146994feb2b5
Binary files /dev/null and b/app/assets/images/emoji/water_polo_tone5.png differ
diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0761488b4c97273e19103ff18a37113b8ce2a499
Binary files /dev/null and b/app/assets/images/emoji/watermelon.png differ
diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0cd79b45f52b42130f9f36b38d4628d49fe3143
Binary files /dev/null and b/app/assets/images/emoji/wave.png differ
diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b2b34b106ea79551bb4b9afbd5a3ef02de23409
Binary files /dev/null and b/app/assets/images/emoji/wave_tone1.png differ
diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b857119732e912f83fcc3ccc530d5a901501db86
Binary files /dev/null and b/app/assets/images/emoji/wave_tone2.png differ
diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..6283b670f4327ebbf55a2332a623d90bb4bd8018
Binary files /dev/null and b/app/assets/images/emoji/wave_tone3.png differ
diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe6b2baa7477a6cd50cf2d7f1f0c374fe0d75ddb
Binary files /dev/null and b/app/assets/images/emoji/wave_tone4.png differ
diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bd168ebb78027bc425afff671e15aeeec3a1222
Binary files /dev/null and b/app/assets/images/emoji/wave_tone5.png differ
diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png
new file mode 100644
index 0000000000000000000000000000000000000000..001c8d6e47d67d86f55970d6bd5d3d95469572ec
Binary files /dev/null and b/app/assets/images/emoji/wavy_dash.png differ
diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..687125173d93d2fb771425a1e4104681c59256e6
Binary files /dev/null and b/app/assets/images/emoji/waxing_crescent_moon.png differ
diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a8081563182847706d3151f6268925d6d683bd3
Binary files /dev/null and b/app/assets/images/emoji/waxing_gibbous_moon.png differ
diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa433e84ba62e9670638546fab1410800810d1f0
Binary files /dev/null and b/app/assets/images/emoji/wc.png differ
diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png
new file mode 100644
index 0000000000000000000000000000000000000000..98bfbd24a16863fa1cc83f87c94ab5be46269b34
Binary files /dev/null and b/app/assets/images/emoji/weary.png differ
diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0d8aa0bfae2e9d9d8729a97468162b077c8fd4e
Binary files /dev/null and b/app/assets/images/emoji/wedding.png differ
diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f19b44257c1f0d9e4fb7ab0b2920d3c48440bf2
Binary files /dev/null and b/app/assets/images/emoji/whale.png differ
diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0df9d3c73a481a67bfd945b653f37e15da1a898f
Binary files /dev/null and b/app/assets/images/emoji/whale2.png differ
diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png
new file mode 100644
index 0000000000000000000000000000000000000000..3666db0016b3c6e7321f456a6fc2102e2bb387aa
Binary files /dev/null and b/app/assets/images/emoji/wheel_of_dharma.png differ
diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e5b2698eacd737e72eba14ed90f4104858ea2ae
Binary files /dev/null and b/app/assets/images/emoji/wheelchair.png differ
diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..e55f087e544d1c31b04e725395870e2a8c5a1f4c
Binary files /dev/null and b/app/assets/images/emoji/white_check_mark.png differ
diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png
new file mode 100644
index 0000000000000000000000000000000000000000..c19e15684dd1daa40922621c04bd305f817f4793
Binary files /dev/null and b/app/assets/images/emoji/white_circle.png differ
diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6af8b6007748973a13850fe10120dccfc89a6bd
Binary files /dev/null and b/app/assets/images/emoji/white_flower.png differ
diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f06c1c79de9c6fbe45a7cb6d404fa3d2760a479
Binary files /dev/null and b/app/assets/images/emoji/white_large_square.png differ
diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae8741267505f553e8b2fa9be9bc2c12f1db2adf
Binary files /dev/null and b/app/assets/images/emoji/white_medium_small_square.png differ
diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..8daacf57059d139fac9792d8e73999968b69a96b
Binary files /dev/null and b/app/assets/images/emoji/white_medium_square.png differ
diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7ebdb0c0ed4d155b7b9b33bf507e1c6dfdd0e15
Binary files /dev/null and b/app/assets/images/emoji/white_small_square.png differ
diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..934b1cedfd27140904e9ac3e33c5b8604298458f
Binary files /dev/null and b/app/assets/images/emoji/white_square_button.png differ
diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a4cc10026962e4aba3b9fcc2584f9c49e0eee6e
Binary files /dev/null and b/app/assets/images/emoji/white_sun_cloud.png differ
diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png
new file mode 100644
index 0000000000000000000000000000000000000000..491f9ca483942e2b3f4f075b92846153235a359c
Binary files /dev/null and b/app/assets/images/emoji/white_sun_rain_cloud.png differ
diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png
new file mode 100644
index 0000000000000000000000000000000000000000..cead0bfa521d06788e9da56682e76d8318c6b913
Binary files /dev/null and b/app/assets/images/emoji/white_sun_small_cloud.png differ
diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png
new file mode 100644
index 0000000000000000000000000000000000000000..62412b143ae6c43590a9466c6d409ca81d2a4d41
Binary files /dev/null and b/app/assets/images/emoji/wilted_rose.png differ
diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png
new file mode 100644
index 0000000000000000000000000000000000000000..df81b652eb65df15fe66d6951a91906d982660d2
Binary files /dev/null and b/app/assets/images/emoji/wind_blowing_face.png differ
diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c9ef3a95f6e541e5a302894d3560ea050208f8a
Binary files /dev/null and b/app/assets/images/emoji/wind_chime.png differ
diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cc986891926e4443f2cbf67824ab0c6f698f6d2
Binary files /dev/null and b/app/assets/images/emoji/wine_glass.png differ
diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ea7810a37db466a80252faa76f77b38d879b2d6
Binary files /dev/null and b/app/assets/images/emoji/wink.png differ
diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba7220f2de9e60ce2c2116d678340df252a386c8
Binary files /dev/null and b/app/assets/images/emoji/wolf.png differ
diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png
new file mode 100644
index 0000000000000000000000000000000000000000..ece440e7a61400e87dff7d07571de3c7c3c4bc7b
Binary files /dev/null and b/app/assets/images/emoji/woman.png differ
diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff089b8889bd0af6859f656d941cb5c0f932183e
Binary files /dev/null and b/app/assets/images/emoji/woman_tone1.png differ
diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0719c378016c5b8436074982789a2ac4c5aae389
Binary files /dev/null and b/app/assets/images/emoji/woman_tone2.png differ
diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5672e2fd52dc57b71ffd5835a9303cafd4adc292
Binary files /dev/null and b/app/assets/images/emoji/woman_tone3.png differ
diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5754aab558b87407a01e12b05b4dab48470aedad
Binary files /dev/null and b/app/assets/images/emoji/woman_tone4.png differ
diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc252af3a3964b496f283b20f90855012d75071f
Binary files /dev/null and b/app/assets/images/emoji/woman_tone5.png differ
diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png
new file mode 100644
index 0000000000000000000000000000000000000000..01410dc8107fe79bb676189c6ac28dc1db9d0acc
Binary files /dev/null and b/app/assets/images/emoji/womans_clothes.png differ
diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png
new file mode 100644
index 0000000000000000000000000000000000000000..b837b6a2e47a13d2dd8d5527bf3d2c5f5562abfb
Binary files /dev/null and b/app/assets/images/emoji/womans_hat.png differ
diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4ecc22e7b3d580db3822fdbe82545dc0f61252c
Binary files /dev/null and b/app/assets/images/emoji/womens.png differ
diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png
new file mode 100644
index 0000000000000000000000000000000000000000..7074afcf5b7b3e5ef27cee8f6087cd34689fd71e
Binary files /dev/null and b/app/assets/images/emoji/worried.png differ
diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png
new file mode 100644
index 0000000000000000000000000000000000000000..c16b743969752d019ea76620af7616e7c53ab667
Binary files /dev/null and b/app/assets/images/emoji/wrench.png differ
diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png
new file mode 100644
index 0000000000000000000000000000000000000000..71e67cfad851b6d4b59ec1687a407aa071bd9027
Binary files /dev/null and b/app/assets/images/emoji/wrestlers.png differ
diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..379070fd03bdd04035a95cef7cf0272364bed6d1
Binary files /dev/null and b/app/assets/images/emoji/wrestlers_tone1.png differ
diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6863ea9209d81c40bdb03de2306f4b460cf42d87
Binary files /dev/null and b/app/assets/images/emoji/wrestlers_tone2.png differ
diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..b7e62910127a313036462214af03b791d2334a9b
Binary files /dev/null and b/app/assets/images/emoji/wrestlers_tone3.png differ
diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..750f9589233fabf1f3a74b0bcb2f5e483801a9ee
Binary files /dev/null and b/app/assets/images/emoji/wrestlers_tone4.png differ
diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..36ab9bb3f4250c516995cbdb7afeb9ca10ebde8d
Binary files /dev/null and b/app/assets/images/emoji/wrestlers_tone5.png differ
diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png
new file mode 100644
index 0000000000000000000000000000000000000000..85639f8ac40638d2a044551f73d9ccb6d8a66b98
Binary files /dev/null and b/app/assets/images/emoji/writing_hand.png differ
diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png
new file mode 100644
index 0000000000000000000000000000000000000000..7923d8ebb17198d96c2355f0459a66279c165e71
Binary files /dev/null and b/app/assets/images/emoji/writing_hand_tone1.png differ
diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcb304e15d200327cfe64f0092b9f2e8430e3c19
Binary files /dev/null and b/app/assets/images/emoji/writing_hand_tone2.png differ
diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd885fd2d9068b780d3c770778194758a474ab98
Binary files /dev/null and b/app/assets/images/emoji/writing_hand_tone3.png differ
diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png
new file mode 100644
index 0000000000000000000000000000000000000000..d065b8c64abd05d579b4c088f63e913979e70fae
Binary files /dev/null and b/app/assets/images/emoji/writing_hand_tone4.png differ
diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a44b3dd757c3d62cb263aa287062dcb2f18a427c
Binary files /dev/null and b/app/assets/images/emoji/writing_hand_tone5.png differ
diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f9ed0f7ad21750c23f43ed9c162e5425939c9e9
Binary files /dev/null and b/app/assets/images/emoji/x.png differ
diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png
new file mode 100644
index 0000000000000000000000000000000000000000..7901a9d0103f4e2c2c4e7206cd9b142b1e0ee221
Binary files /dev/null and b/app/assets/images/emoji/yellow_heart.png differ
diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png
new file mode 100644
index 0000000000000000000000000000000000000000..63ee4799d66f66db9cc2d00bed2af2074202ffe0
Binary files /dev/null and b/app/assets/images/emoji/yen.png differ
diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2900f6338ffe40e63d820f130dfc7dfe6ef88de
Binary files /dev/null and b/app/assets/images/emoji/yin_yang.png differ
diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png
new file mode 100644
index 0000000000000000000000000000000000000000..2df15753ca15886922425e0ecda28faddb0c5aa4
Binary files /dev/null and b/app/assets/images/emoji/yum.png differ
diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png
new file mode 100644
index 0000000000000000000000000000000000000000..47e68e48e49e4e09bfb5712c6970d2f1a979b46d
Binary files /dev/null and b/app/assets/images/emoji/zap.png differ
diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png
new file mode 100644
index 0000000000000000000000000000000000000000..13aca83e018e8ffcc06fc9d1dde58adbdcf39a78
Binary files /dev/null and b/app/assets/images/emoji/zero.png differ
diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8ced2502a74389ffd107102af0f7df5ea4a2f31
Binary files /dev/null and b/app/assets/images/emoji/zipper_mouth.png differ
diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bc72b4469f95b1a744dc36bfbb8c16e192ab0f9
Binary files /dev/null and b/app/assets/images/emoji/zzz.png differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index dc9cae1d44cf0282dab99202430b4ae0c710d37a..b0fa9e1139eac2e52be6d31e25b16d2bb61ee640 100644
Binary files a/app/assets/images/emoji@2x.png and b/app/assets/images/emoji@2x.png differ
diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c4d8e65122d724f783a5d4b50c7b2258755b3f29
--- /dev/null
+++ b/app/assets/images/icon-merge-request-unmerged.svg
@@ -0,0 +1 @@
+<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg>
diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3f4ef31947bc4a53d6b2d243d6e9f2c975c84b77
Binary files /dev/null and b/app/assets/images/mailers/gitlab_footer_logo.gif differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif
new file mode 100644
index 0000000000000000000000000000000000000000..387628f831c2ef2d14863a2e30bde7c6f3e5bd85
Binary files /dev/null and b/app/assets/images/mailers/gitlab_header_logo.gif differ
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
new file mode 100644
index 0000000000000000000000000000000000000000..346de4ad11e9ed0658aee3f6c8f2501cff9224a7
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js
@@ -0,0 +1,37 @@
+const MAX_MESSAGE_LENGTH = 500;
+const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
+
+class AbuseReports {
+  constructor() {
+    $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+    $(document)
+      .off('click', MESSAGE_CELL_SELECTOR)
+      .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+  }
+
+  truncateLongMessage() {
+    const $messageCellElement = $(this);
+    const reportMessage = $messageCellElement.text();
+    if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+      $messageCellElement.data('original-message', reportMessage);
+      $messageCellElement.data('message-truncated', 'true');
+      $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+    }
+  }
+
+  toggleMessageTruncation() {
+    const $messageCellElement = $(this);
+    const originalMessage = $messageCellElement.data('original-message');
+    if (!originalMessage) return;
+    if ($messageCellElement.data('message-truncated') === 'true') {
+      $messageCellElement.data('message-truncated', 'false');
+      $messageCellElement.text(originalMessage);
+    } else {
+      $messageCellElement.data('message-truncated', 'true');
+      $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+    }
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.AbuseReports = AbuseReports;
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
deleted file mode 100644
index 8a260aae1b15de8ae6c4ae993a41a8025562f824..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ /dev/null
@@ -1,40 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-((global) => {
-  const MAX_MESSAGE_LENGTH = 500;
-  const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-
-  class AbuseReports {
-    constructor() {
-      $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
-      $(document)
-        .off('click', MESSAGE_CELL_SELECTOR)
-        .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
-    }
-
-    truncateLongMessage() {
-      const $messageCellElement = $(this);
-      const reportMessage = $messageCellElement.text();
-      if (reportMessage.length > MAX_MESSAGE_LENGTH) {
-        $messageCellElement.data('original-message', reportMessage);
-        $messageCellElement.data('message-truncated', 'true');
-        $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
-      }
-    }
-
-    toggleMessageTruncation() {
-      const $messageCellElement = $(this);
-      const originalMessage = $messageCellElement.data('original-message');
-      if (!originalMessage) return;
-      if ($messageCellElement.data('message-truncated') === 'true') {
-        $messageCellElement.data('message-truncated', 'false');
-        $messageCellElement.text(originalMessage);
-      } else {
-        $messageCellElement.data('message-truncated', 'true');
-        $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
-      }
-    }
-  }
-
-  global.AbuseReports = AbuseReports;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
new file mode 100644
index 0000000000000000000000000000000000000000..aebda7780e1e02a31f972cf0a652c982cfc595c3
--- /dev/null
+++ b/app/assets/javascripts/activities.js
@@ -0,0 +1,36 @@
+/* eslint-disable no-param-reassign, class-methods-use-this */
+/* global Pager */
+/* global Cookies */
+
+class Activities {
+  constructor() {
+    Pager.init(20, true, false, this.updateTooltips);
+    $('.event-filter-link').on('click', (e) => {
+      e.preventDefault();
+      this.toggleFilter(e.currentTarget);
+      this.reloadActivities();
+    });
+  }
+
+  updateTooltips() {
+    gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+  }
+
+  reloadActivities() {
+    $('.content_list').html('');
+    Pager.init(20, true, false, this.updateTooltips);
+  }
+
+  toggleFilter(sender) {
+    const $sender = $(sender);
+    const filter = $sender.attr('id').split('_')[0];
+
+    $('.event-filter .active').removeClass('active');
+    Cookies.set('event_filter', filter);
+
+    $sender.closest('li').toggleClass('active');
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.Activities = Activities;
diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6
deleted file mode 100644
index 648cb4d5d854a9433404b745e9f251471b71982d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/activities.js.es6
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable no-param-reassign, class-methods-use-this */
-/* global Pager */
-/* global Cookies */
-
-((global) => {
-  class Activities {
-    constructor() {
-      Pager.init(20, true, false, this.updateTooltips);
-      $('.event-filter-link').on('click', (e) => {
-        e.preventDefault();
-        this.toggleFilter(e.currentTarget);
-        this.reloadActivities();
-      });
-    }
-
-    updateTooltips() {
-      gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
-    }
-
-    reloadActivities() {
-      $('.content_list').html('');
-      Pager.init(20, true, false, this.updateTooltips);
-    }
-
-    toggleFilter(sender) {
-      const $sender = $(sender);
-      const filter = $sender.attr('id').split('_')[0];
-
-      $('.event-filter .active').removeClass('active');
-      Cookies.set('event_filter', filter);
-
-      $sender.closest('li').toggleClass('active');
-    }
-  }
-
-  global.Activities = Activities;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index aaed74d6073d6df857c88dfa47618e9d7ead0240..34669dd13d65e9e9d5235a61565bf36b042b46c2 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,64 +1,62 @@
 /* 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, max-len */
 
-(function() {
-  this.Admin = (function() {
-    function Admin() {
-      var modal, showBlacklistType;
-      $('input#user_force_random_password').on('change', function(elem) {
-        var elems;
-        elems = $('#user_password, #user_password_confirmation');
-        if ($(this).attr('checked')) {
-          return elems.val('').attr('disabled', true);
-        } else {
-          return elems.removeAttr('disabled');
-        }
-      });
-      $('body').on('click', '.js-toggle-colors-link', function(e) {
-        e.preventDefault();
-        return $('.js-toggle-colors-container').toggle();
-      });
-      $('.log-tabs a').click(function(e) {
-        e.preventDefault();
-        return $(this).tab('show');
-      });
-      $('.log-bottom').click(function(e) {
-        var visible_log;
-        e.preventDefault();
-        visible_log = $(".file-content:visible");
-        return visible_log.animate({
-          scrollTop: visible_log.find('ol').height()
-        }, "fast");
-      });
-      modal = $('.change-owner-holder');
-      $('.change-owner-link').bind("click", function(e) {
-        e.preventDefault();
-        $(this).hide();
-        return modal.show();
-      });
-      $('.change-owner-cancel-link').bind("click", function(e) {
-        e.preventDefault();
-        modal.hide();
-        return $('.change-owner-link').show();
-      });
-      $('li.project_member').bind('ajax:success', function() {
-        return gl.utils.refreshCurrentPage();
-      });
-      $('li.group_member').bind('ajax:success', function() {
-        return gl.utils.refreshCurrentPage();
-      });
-      showBlacklistType = function() {
-        if ($("input[name='blacklist_type']:checked").val() === 'file') {
-          $('.blacklist-file').show();
-          return $('.blacklist-raw').hide();
-        } else {
-          $('.blacklist-file').hide();
-          return $('.blacklist-raw').show();
-        }
-      };
-      $("input[name='blacklist_type']").click(showBlacklistType);
-      showBlacklistType();
-    }
+window.Admin = (function() {
+  function Admin() {
+    var modal, showBlacklistType;
+    $('input#user_force_random_password').on('change', function(elem) {
+      var elems;
+      elems = $('#user_password, #user_password_confirmation');
+      if ($(this).attr('checked')) {
+        return elems.val('').attr('disabled', true);
+      } else {
+        return elems.removeAttr('disabled');
+      }
+    });
+    $('body').on('click', '.js-toggle-colors-link', function(e) {
+      e.preventDefault();
+      return $('.js-toggle-colors-container').toggle();
+    });
+    $('.log-tabs a').click(function(e) {
+      e.preventDefault();
+      return $(this).tab('show');
+    });
+    $('.log-bottom').click(function(e) {
+      var visible_log;
+      e.preventDefault();
+      visible_log = $(".file-content:visible");
+      return visible_log.animate({
+        scrollTop: visible_log.find('ol').height()
+      }, "fast");
+    });
+    modal = $('.change-owner-holder');
+    $('.change-owner-link').bind("click", function(e) {
+      e.preventDefault();
+      $(this).hide();
+      return modal.show();
+    });
+    $('.change-owner-cancel-link').bind("click", function(e) {
+      e.preventDefault();
+      modal.hide();
+      return $('.change-owner-link').show();
+    });
+    $('li.project_member').bind('ajax:success', function() {
+      return gl.utils.refreshCurrentPage();
+    });
+    $('li.group_member').bind('ajax:success', function() {
+      return gl.utils.refreshCurrentPage();
+    });
+    showBlacklistType = function() {
+      if ($("input[name='blacklist_type']:checked").val() === 'file') {
+        $('.blacklist-file').show();
+        return $('.blacklist-raw').hide();
+      } else {
+        $('.blacklist-file').hide();
+        return $('.blacklist-raw').show();
+      }
+    };
+    $("input[name='blacklist_type']").click(showBlacklistType);
+    showBlacklistType();
+  }
 
-    return Admin;
-  })();
-}).call(window);
+  return Admin;
+})();
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
new file mode 100644
index 0000000000000000000000000000000000000000..38a8317dbd7c6df736f1f4bc9de2b761a6a32618
--- /dev/null
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -0,0 +1,35 @@
+class AjaxLoadingSpinner {
+  static init() {
+    const $elements = $('.js-ajax-loading-spinner');
+
+    $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+    $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+  }
+
+  static ajaxBeforeSend(e) {
+    e.target.setAttribute('disabled', '');
+    const iconElement = e.target.querySelector('i');
+    // get first fa- icon
+    const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first();
+    iconElement.dataset.icon = originalIcon;
+    AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+    $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+  }
+
+  static ajaxComplete(e) {
+    e.target.removeAttribute('disabled');
+    const iconElement = e.target.querySelector('i');
+    AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+    $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+  }
+
+  static toggleLoadingIcon(iconElement) {
+    const classList = iconElement.classList;
+    classList.toggle(iconElement.dataset.icon);
+    classList.toggle('fa-spinner');
+    classList.toggle('fa-spin');
+  }
+}
+
+window.gl = window.gl || {};
+gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 86e0ad894319e64ccc8922bfd5702fd64cd39d75..e5f36c849874d7da4841a332b470720d68c09e72 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,150 +1,148 @@
 /* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
 
-(function() {
-  var Api = {
-    groupsPath: "/api/:version/groups.json",
-    groupPath: "/api/:version/groups/:id.json",
-    namespacesPath: "/api/:version/namespaces.json",
-    groupProjectsPath: "/api/:version/groups/:id/projects.json",
-    projectsPath: "/api/:version/projects.json?simple=true",
-    labelsPath: "/:namespace_path/:project_path/labels",
-    licensePath: "/api/:version/templates/licenses/:key",
-    gitignorePath: "/api/:version/templates/gitignores/:key",
-    gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
-    dockerfilePath: "/api/:version/templates/dockerfiles/:key",
-    issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
-    group: function(group_id, callback) {
-      var url = Api.buildUrl(Api.groupPath)
-        .replace(':id', group_id);
-      return $.ajax({
-        url: url,
-        dataType: "json"
-      }).done(function(group) {
-        return callback(group);
-      });
-    },
-    // Return groups list. Filtered by query
-    groups: function(query, options, callback) {
-      var url = Api.buildUrl(Api.groupsPath);
-      return $.ajax({
-        url: url,
-        data: $.extend({
-          search: query,
-          per_page: 20
-        }, options),
-        dataType: "json"
-      }).done(function(groups) {
-        return callback(groups);
-      });
-    },
-    // Return namespaces list. Filtered by query
-    namespaces: function(query, callback) {
-      var url = Api.buildUrl(Api.namespacesPath);
-      return $.ajax({
-        url: url,
-        data: {
-          search: query,
-          per_page: 20
-        },
-        dataType: "json"
-      }).done(function(namespaces) {
-        return callback(namespaces);
-      });
-    },
-    // Return projects list. Filtered by query
-    projects: function(query, order, callback) {
-      var url = Api.buildUrl(Api.projectsPath);
-      return $.ajax({
-        url: url,
-        data: {
-          search: query,
-          order_by: order,
-          per_page: 20
-        },
-        dataType: "json"
-      }).done(function(projects) {
-        return callback(projects);
-      });
-    },
-    newLabel: function(namespace_path, project_path, data, callback) {
-      var url = Api.buildUrl(Api.labelsPath)
-        .replace(':namespace_path', namespace_path)
-        .replace(':project_path', project_path);
-      return $.ajax({
-        url: url,
-        type: "POST",
-        data: { 'label': data },
-        dataType: "json"
-      }).done(function(label) {
-        return callback(label);
-      }).error(function(message) {
-        return callback(message.responseJSON);
-      });
-    },
-    // Return group projects list. Filtered by query
-    groupProjects: function(group_id, query, callback) {
-      var url = Api.buildUrl(Api.groupProjectsPath)
-        .replace(':id', group_id);
-      return $.ajax({
-        url: url,
-        data: {
-          search: query,
-          per_page: 20
-        },
-        dataType: "json"
-      }).done(function(projects) {
-        return callback(projects);
-      });
-    },
-    // Return text for a specific license
-    licenseText: function(key, data, callback) {
-      var url = Api.buildUrl(Api.licensePath)
-        .replace(':key', key);
-      return $.ajax({
-        url: url,
-        data: data
-      }).done(function(license) {
-        return callback(license);
-      });
-    },
-    gitignoreText: function(key, callback) {
-      var url = Api.buildUrl(Api.gitignorePath)
-        .replace(':key', key);
-      return $.get(url, function(gitignore) {
-        return callback(gitignore);
-      });
-    },
-    gitlabCiYml: function(key, callback) {
-      var url = Api.buildUrl(Api.gitlabCiYmlPath)
-        .replace(':key', key);
-      return $.get(url, function(file) {
-        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)
-        .replace(':type', type)
-        .replace(':project_path', projectPath)
-        .replace(':namespace_path', namespacePath);
-      $.ajax({
-        url: url,
-        dataType: 'json'
-      }).done(function(file) {
-        callback(null, file);
-      }).error(callback);
-    },
-    buildUrl: function(url) {
-      if (gon.relative_url_root != null) {
-        url = gon.relative_url_root + url;
-      }
-      return url.replace(':version', gon.api_version);
+var Api = {
+  groupsPath: "/api/:version/groups.json",
+  groupPath: "/api/:version/groups/:id.json",
+  namespacesPath: "/api/:version/namespaces.json",
+  groupProjectsPath: "/api/:version/groups/:id/projects.json",
+  projectsPath: "/api/:version/projects.json?simple=true",
+  labelsPath: "/:namespace_path/:project_path/labels",
+  licensePath: "/api/:version/templates/licenses/:key",
+  gitignorePath: "/api/:version/templates/gitignores/:key",
+  gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+  dockerfilePath: "/api/:version/templates/dockerfiles/:key",
+  issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+  group: function(group_id, callback) {
+    var url = Api.buildUrl(Api.groupPath)
+      .replace(':id', group_id);
+    return $.ajax({
+      url: url,
+      dataType: "json"
+    }).done(function(group) {
+      return callback(group);
+    });
+  },
+  // Return groups list. Filtered by query
+  groups: function(query, options, callback) {
+    var url = Api.buildUrl(Api.groupsPath);
+    return $.ajax({
+      url: url,
+      data: $.extend({
+        search: query,
+        per_page: 20
+      }, options),
+      dataType: "json"
+    }).done(function(groups) {
+      return callback(groups);
+    });
+  },
+  // Return namespaces list. Filtered by query
+  namespaces: function(query, callback) {
+    var url = Api.buildUrl(Api.namespacesPath);
+    return $.ajax({
+      url: url,
+      data: {
+        search: query,
+        per_page: 20
+      },
+      dataType: "json"
+    }).done(function(namespaces) {
+      return callback(namespaces);
+    });
+  },
+  // Return projects list. Filtered by query
+  projects: function(query, options, callback) {
+    var url = Api.buildUrl(Api.projectsPath);
+    return $.ajax({
+      url: url,
+      data: $.extend({
+        search: query,
+        per_page: 20,
+        membership: true
+      }, options),
+      dataType: "json"
+    }).done(function(projects) {
+      return callback(projects);
+    });
+  },
+  newLabel: function(namespace_path, project_path, data, callback) {
+    var url = Api.buildUrl(Api.labelsPath)
+      .replace(':namespace_path', namespace_path)
+      .replace(':project_path', project_path);
+    return $.ajax({
+      url: url,
+      type: "POST",
+      data: { 'label': data },
+      dataType: "json"
+    }).done(function(label) {
+      return callback(label);
+    }).error(function(message) {
+      return callback(message.responseJSON);
+    });
+  },
+  // Return group projects list. Filtered by query
+  groupProjects: function(group_id, query, callback) {
+    var url = Api.buildUrl(Api.groupProjectsPath)
+      .replace(':id', group_id);
+    return $.ajax({
+      url: url,
+      data: {
+        search: query,
+        per_page: 20
+      },
+      dataType: "json"
+    }).done(function(projects) {
+      return callback(projects);
+    });
+  },
+  // Return text for a specific license
+  licenseText: function(key, data, callback) {
+    var url = Api.buildUrl(Api.licensePath)
+      .replace(':key', key);
+    return $.ajax({
+      url: url,
+      data: data
+    }).done(function(license) {
+      return callback(license);
+    });
+  },
+  gitignoreText: function(key, callback) {
+    var url = Api.buildUrl(Api.gitignorePath)
+      .replace(':key', key);
+    return $.get(url, function(gitignore) {
+      return callback(gitignore);
+    });
+  },
+  gitlabCiYml: function(key, callback) {
+    var url = Api.buildUrl(Api.gitlabCiYmlPath)
+      .replace(':key', key);
+    return $.get(url, function(file) {
+      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)
+      .replace(':type', type)
+      .replace(':project_path', projectPath)
+      .replace(':namespace_path', namespacePath);
+    $.ajax({
+      url: url,
+      dataType: 'json'
+    }).done(function(file) {
+      callback(null, file);
+    }).error(callback);
+  },
+  buildUrl: function(url) {
+    if (gon.relative_url_root != null) {
+      url = gon.relative_url_root + url;
     }
-  };
+    return url.replace(':version', gon.api_version);
+  }
+};
 
-  window.Api = Api;
-}).call(window);
+window.Api = Api;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
deleted file mode 100644
index 8e468faedbf4f44ddd54345e984c3e2bc8dc9a16..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */
-/* global bp */
-/* global Cookies */
-/* global Flash */
-/* global ConfirmDangerModal */
-/* global AwardsHandler */
-/* global Aside */
-
-function requireAll(context) { return context.keys().map(context); }
-
-window.$ = window.jQuery = require('jquery');
-require('jquery-ui/ui/autocomplete');
-require('jquery-ui/ui/draggable');
-require('jquery-ui/ui/effect-highlight');
-require('jquery-ui/ui/sortable');
-require('jquery-ujs');
-require('vendor/jquery.endless-scroll');
-require('vendor/jquery.highlight');
-require('vendor/jquery.waitforimages');
-require('vendor/jquery.caret');
-require('vendor/jquery.atwho');
-require('vendor/jquery.scrollTo');
-window.Cookies = require('js-cookie');
-require('./autosave');
-require('bootstrap/js/affix');
-require('bootstrap/js/alert');
-require('bootstrap/js/button');
-require('bootstrap/js/collapse');
-require('bootstrap/js/dropdown');
-require('bootstrap/js/modal');
-require('bootstrap/js/scrollspy');
-require('bootstrap/js/tab');
-require('bootstrap/js/transition');
-require('bootstrap/js/tooltip');
-require('bootstrap/js/popover');
-require('select2/select2.js');
-window.Pikaday = require('pikaday');
-window._ = require('underscore');
-window.Dropzone = require('dropzone');
-window.Sortable = require('vendor/Sortable');
-require('mousetrap');
-require('mousetrap/plugins/pause/mousetrap-pause');
-require('./shortcuts');
-require('./shortcuts_navigation');
-require('./shortcuts_dashboard_navigation');
-require('./shortcuts_issuable');
-require('./shortcuts_network');
-require('vendor/jquery.nicescroll');
-requireAll(require.context('./behaviors',  false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./blob',       false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./templates',  false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./commit',     false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./lib/utils',  false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./u2f',        false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./droplab',    false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('.',            false, /^\.\/(?!application\.js).*\.(js|es6)$/));
-require('vendor/fuzzaldrin-plus');
-require('es6-promise').polyfill();
-
-(function () {
-  document.addEventListener('beforeunload', function () {
-    // Unbind scroll events
-    $(document).off('scroll');
-    // Close any open tooltips
-    $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
-  });
-
-  window.addEventListener('hashchange', gl.utils.handleLocationHash);
-  window.addEventListener('load', function onLoad() {
-    window.removeEventListener('load', onLoad, false);
-    gl.utils.handleLocationHash();
-  }, false);
-
-  $(function () {
-    var $body = $('body');
-    var $document = $(document);
-    var $window = $(window);
-    var $sidebarGutterToggle = $('.js-sidebar-toggle');
-    var $flash = $('.flash-container');
-    var bootstrapBreakpoint = bp.getBreakpointSize();
-    var fitSidebarForSize;
-
-    // Set the default path for all cookies to GitLab's root directory
-    Cookies.defaults.path = gon.relative_url_root || '/';
-
-    // `hashchange` is not triggered when link target is already in window.location
-    $body.on('click', 'a[href^="#"]', function() {
-      var href = this.getAttribute('href');
-      if (href.substr(1) === gl.utils.getLocationHash()) {
-        setTimeout(gl.utils.handleLocationHash, 1);
-      }
-    });
-
-    // prevent default action for disabled buttons
-    $('.btn').click(function(e) {
-      if ($(this).hasClass('disabled')) {
-        e.preventDefault();
-        e.stopImmediatePropagation();
-        return false;
-      }
-    });
-
-    $('.js-select-on-focus').on('focusin', function () {
-      return $(this).select().one('mouseup', function (e) {
-        return e.preventDefault();
-      });
-    // Click a .js-select-on-focus field, select the contents
-    // Prevent a mouseup event from deselecting the input
-    });
-    $('.remove-row').bind('ajax:success', function () {
-      $(this).tooltip('destroy')
-        .closest('li')
-        .fadeOut();
-    });
-    $('.js-remove-tr').bind('ajax:before', function () {
-      return $(this).hide();
-    });
-    $('.js-remove-tr').bind('ajax:success', function () {
-      return $(this).closest('tr').fadeOut();
-    });
-    $('select.select2').select2({
-      width: 'resolve',
-      // Initialize select2 selects
-      dropdownAutoWidth: true
-    });
-    $('.js-select2').bind('select2-close', function () {
-      return setTimeout((function () {
-        $('.select2-container-active').removeClass('select2-container-active');
-        return $(':focus').blur();
-      }), 1);
-    // Close select2 on escape
-    });
-    // Initialize tooltips
-    $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
-    $body.tooltip({
-      selector: '.has-tooltip, [data-toggle="tooltip"]',
-      placement: function (_, el) {
-        return $(el).data('placement') || 'bottom';
-      }
-    });
-    $('.trigger-submit').on('change', function () {
-      return $(this).parents('form').submit();
-    // Form submitter
-    });
-    gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
-    // Flash
-    if ($flash.length > 0) {
-      $flash.click(function () {
-        return $(this).fadeOut();
-      });
-      $flash.show();
-    }
-    // Disable form buttons while a form is submitting
-    $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
-      var buttons;
-      buttons = $('[type="submit"]', this);
-      switch (e.type) {
-        case 'ajax:beforeSend':
-        case 'submit':
-          return buttons.disable();
-        default:
-          return buttons.enable();
-      }
-    });
-    $(document).ajaxError(function (e, xhrObj) {
-      var ref = xhrObj.status;
-      if (xhrObj.status === 401) {
-        return new Flash('You need to be logged in.', 'alert');
-      } else if (ref === 404 || ref === 500) {
-        return new Flash('Something went wrong on our end.', 'alert');
-      }
-    });
-    $('.account-box').hover(function () {
-      // Show/Hide the profile menu when hovering the account box
-      return $(this).toggleClass('hover');
-    });
-    $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
-      var $container;
-      $container = $(this).parent();
-      $container.next('table').show();
-      return $container.remove();
-    // Commit show suppressed diff
-    });
-    $('.navbar-toggle').on('click', function () {
-      $('.header-content .title').toggle();
-      $('.header-content .header-logo').toggle();
-      $('.header-content .navbar-collapse').toggle();
-      return $('.navbar-toggle').toggleClass('active');
-    });
-    // Show/hide comments on diff
-    $body.on('click', '.js-toggle-diff-comments', function (e) {
-      var $this = $(this);
-      var notesHolders = $this.closest('.diff-file').find('.notes_holder');
-      $this.toggleClass('active');
-      if ($this.hasClass('active')) {
-        notesHolders.show().find('.hide').show();
-      } else {
-        notesHolders.hide();
-      }
-      $this.trigger('blur');
-      return e.preventDefault();
-    });
-    $document.off('click', '.js-confirm-danger');
-    $document.on('click', '.js-confirm-danger', function (e) {
-      var btn = $(e.target);
-      var form = btn.closest('form');
-      var text = btn.data('confirm-danger-message');
-      e.preventDefault();
-      return new ConfirmDangerModal(form, text);
-    });
-    $('input[type="search"]').each(function () {
-      var $this = $(this);
-      $this.attr('value', $this.val());
-    });
-    $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
-      var $this;
-      $this = $(this);
-      return $this.attr('value', $this.val());
-    });
-    $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
-      var $gutterIcon;
-      if (breakpoint === 'sm' || breakpoint === 'xs') {
-        $gutterIcon = $sidebarGutterToggle.find('i');
-        if ($gutterIcon.hasClass('fa-angle-double-right')) {
-          return $sidebarGutterToggle.trigger('click');
-        }
-      }
-    });
-    fitSidebarForSize = function () {
-      var oldBootstrapBreakpoint;
-      oldBootstrapBreakpoint = bootstrapBreakpoint;
-      bootstrapBreakpoint = bp.getBreakpointSize();
-      if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
-        return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
-      }
-    };
-    $window.off('resize.app').on('resize.app', function () {
-      return fitSidebarForSize();
-    });
-    gl.awardsHandler = new AwardsHandler();
-    new Aside();
-
-    gl.utils.initTimeagoTimeout();
-  });
-}).call(window);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 448e6e2cc78764a72ce16b7d4e4998df51fcd781..88756884d161740abc650207076ab93c1e901211 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,25 +1,24 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
-  this.Aside = (function() {
-    function Aside() {
-      $(document).off("click", "a.show-aside");
-      $(document).on("click", 'a.show-aside', function(e) {
-        var btn, icon;
-        e.preventDefault();
-        btn = $(e.currentTarget);
-        icon = btn.find('i');
-        if (icon.hasClass('fa-angle-left')) {
-          btn.parent().find('section').hide();
-          btn.parent().find('aside').fadeIn();
-          return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
-        } else {
-          btn.parent().find('aside').hide();
-          btn.parent().find('section').fadeIn();
-          return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
-        }
-      });
-    }
 
-    return Aside;
-  })();
-}).call(window);
+window.Aside = (function() {
+  function Aside() {
+    $(document).off("click", "a.show-aside");
+    $(document).on("click", 'a.show-aside', function(e) {
+      var btn, icon;
+      e.preventDefault();
+      btn = $(e.currentTarget);
+      icon = btn.find('i');
+      if (icon.hasClass('fa-angle-left')) {
+        btn.parent().find('section').hide();
+        btn.parent().find('aside').fadeIn();
+        return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
+      } else {
+        btn.parent().find('aside').hide();
+        btn.parent().find('section').fadeIn();
+        return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
+      }
+    });
+  }
+
+  return Aside;
+})();
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index e55405135fb9de1951942000a2b045f808628916..8630b18a73f96b91028dec68f5d258dca7239346 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,62 +1,61 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
-(function() {
-  this.Autosave = (function() {
-    function Autosave(field, key) {
-      this.field = field;
-      if (key.join != null) {
-        key = key.join("/");
-      }
-      this.key = "autosave/" + key;
-      this.field.data("autosave", this);
-      this.restore();
-      this.field.on("input", (function(_this) {
-        return function() {
-          return _this.save();
-        };
-      })(this));
-    }
 
-    Autosave.prototype.restore = function() {
-      var e, text;
-      if (window.localStorage == null) {
-        return;
-      }
-      try {
-        text = window.localStorage.getItem(this.key);
-      } catch (error) {
-        e = error;
-        return;
-      }
-      if ((text != null ? text.length : void 0) > 0) {
-        this.field.val(text);
-      }
-      return this.field.trigger("input");
-    };
+window.Autosave = (function() {
+  function Autosave(field, key) {
+    this.field = field;
+    if (key.join != null) {
+      key = key.join("/");
+    }
+    this.key = "autosave/" + key;
+    this.field.data("autosave", this);
+    this.restore();
+    this.field.on("input", (function(_this) {
+      return function() {
+        return _this.save();
+      };
+    })(this));
+  }
 
-    Autosave.prototype.save = function() {
-      var text;
-      if (window.localStorage == null) {
-        return;
-      }
-      text = this.field.val();
-      if ((text != null ? text.length : void 0) > 0) {
-        try {
-          return window.localStorage.setItem(this.key, text);
-        } catch (error) {}
-      } else {
-        return this.reset();
-      }
-    };
+  Autosave.prototype.restore = function() {
+    var e, text;
+    if (window.localStorage == null) {
+      return;
+    }
+    try {
+      text = window.localStorage.getItem(this.key);
+    } catch (error) {
+      e = error;
+      return;
+    }
+    if ((text != null ? text.length : void 0) > 0) {
+      this.field.val(text);
+    }
+    return this.field.trigger("input");
+  };
 
-    Autosave.prototype.reset = function() {
-      if (window.localStorage == null) {
-        return;
-      }
+  Autosave.prototype.save = function() {
+    var text;
+    if (window.localStorage == null) {
+      return;
+    }
+    text = this.field.val();
+    if ((text != null ? text.length : void 0) > 0) {
       try {
-        return window.localStorage.removeItem(this.key);
+        return window.localStorage.setItem(this.key, text);
       } catch (error) {}
-    };
+    } else {
+      return this.reset();
+    }
+  };
+
+  Autosave.prototype.reset = function() {
+    if (window.localStorage == null) {
+      return;
+    }
+    try {
+      return window.localStorage.removeItem(this.key);
+    } catch (error) {}
+  };
 
-    return Autosave;
-  })();
-}).call(window);
+  return Autosave;
+})();
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index a4ccb30e447aa220faae5529ef0e1b13cada0974..9349918f7a077252575f50fa3d1608b97e37971f 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,380 +1,518 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, 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, brace-style, no-underscore-dangle, no-return-assign, camelcase */
 /* global Cookies */
 
-var emojiAliases = require('emoji-aliases');
-
-(function() {
-  this.AwardsHandler = (function() {
-    var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
-    function AwardsHandler() {
-      this.aliases = emojiAliases;
-      $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
-        return function(e) {
-          e.stopPropagation();
-          e.preventDefault();
-          return _this.showEmojiMenu($(e.currentTarget));
-        };
-      })(this));
-      $('html').on('click', function(e) {
-        var $target;
-        $target = $(e.target);
-        if (!$target.closest('.emoji-menu-content').length) {
-          $('.js-awards-block.current').removeClass('current');
-        }
-        if (!$target.closest('.emoji-menu').length) {
-          if ($('.emoji-menu').is(':visible')) {
-            $('.js-add-award.is-active').removeClass('is-active');
-            return $('.emoji-menu').removeClass('is-visible');
-          }
-        }
-      });
-      $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) {
-        return function(e) {
-          var $target, emoji;
-          e.preventDefault();
-          $target = $(e.currentTarget);
-          emoji = $target.find('.icon').data('emoji');
-          $target.closest('.js-awards-block').addClass('current');
-          return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji);
-        };
-      })(this));
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from './behaviors/gl_emoji';
+import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
+
+const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
+const requestAnimationFrame = window.requestAnimationFrame ||
+  window.webkitRequestAnimationFrame ||
+  window.mozRequestAnimationFrame ||
+  window.setTimeout;
+
+const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
+
+let categoryMap = null;
+
+const categoryLabelMap = {
+  activity: 'Activity',
+  people: 'People',
+  nature: 'Nature',
+  food: 'Food',
+  travel: 'Travel',
+  objects: 'Objects',
+  symbols: 'Symbols',
+  flags: 'Flags',
+};
+
+function buildCategoryMap() {
+  return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
+    const emojiInfo = emojiMap[emojiNameKey];
+    if (currentCategoryMap[emojiInfo.category]) {
+      currentCategoryMap[emojiInfo.category].push(emojiNameKey);
     }
 
-    AwardsHandler.prototype.showEmojiMenu = function($addBtn) {
-      var $holder, $menu, url;
-      $menu = $('.emoji-menu');
-      if ($addBtn.hasClass('js-note-emoji')) {
-        $addBtn.closest('.note').find('.js-awards-block').addClass('current');
-      } else {
-        $addBtn.closest('.js-awards-block').addClass('current');
-      }
-      if ($menu.length) {
-        $holder = $addBtn.closest('.js-award-holder');
-        if ($menu.is('.is-visible')) {
-          $addBtn.removeClass('is-active');
-          $menu.removeClass('is-visible');
-          return $('#emoji_search').blur();
-        } else {
-          $addBtn.addClass('is-active');
-          this.positionMenu($menu, $addBtn);
-          $menu.addClass('is-visible');
-          return $('#emoji_search').focus();
-        }
-      } else {
-        $addBtn.addClass('is-loading is-active');
-        url = this.getAwardMenuUrl();
-        return this.createEmojiMenu(url, (function(_this) {
-          return function() {
-            $addBtn.removeClass('is-loading');
-            $menu = $('.emoji-menu');
-            _this.positionMenu($menu, $addBtn);
-            if (!_this.frequentEmojiBlockRendered) {
-              _this.renderFrequentlyUsedBlock();
-            }
-            return setTimeout(function() {
-              $menu.addClass('is-visible');
-              $('#emoji_search').focus();
-              return _this.setupSearch();
-            }, 200);
-          };
-        })(this));
-      }
-    };
-
-    AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) {
-      return $.get(awardMenuUrl, function(response) {
-        $('body').append(response);
-        return callback();
+    return currentCategoryMap;
+  }, {
+    activity: [],
+    people: [],
+    nature: [],
+    food: [],
+    travel: [],
+    objects: [],
+    symbols: [],
+    flags: [],
+  });
+}
+
+function renderCategory(name, emojiList, opts = {}) {
+  return `
+    <h5 class="emoji-menu-title">
+      ${name}
+    </h5>
+    <ul class="clearfix emoji-menu-list ${opts.menuListClass}">
+      ${emojiList.map(emojiName => `
+        <li class="emoji-menu-list-item">
+          <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
+            ${glEmojiTag(emojiName, {
+              sprite: true,
+            })}
+          </button>
+        </li>
+      `).join('\n')}
+    </ul>
+  `;
+}
+
+function AwardsHandler() {
+  this.eventListeners = [];
+  this.aliases = emojiAliases;
+  // If the user shows intent let's pre-build the menu
+  this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
+    const $menu = $('.emoji-menu');
+    if ($menu.length === 0) {
+      requestAnimationFrame(() => {
+        this.createEmojiMenu();
       });
-    };
-
-    AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
-      var css, position;
-      position = $addBtn.data('position');
-      // The menu could potentially be off-screen or in a hidden overflow element
-      // So we position the element absolute in the body
-      css = {
-        top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
-      };
-      if (position === 'right') {
-        css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
-        $menu.addClass('is-aligned-right');
-      } else {
-        css.left = ($addBtn.offset().left) + "px";
-        $menu.removeClass('is-aligned-right');
-      }
-      return $menu.css(css);
-    };
-
-    AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) {
-      if (checkMutuality == null) {
-        checkMutuality = true;
-      }
-      emoji = this.normilizeEmojiName(emoji);
-      this.postEmoji(awardUrl, emoji, (function(_this) {
-        return function() {
-          _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality);
-          return typeof callback === "function" ? callback() : void 0;
-        };
-      })(this));
-      return $('.emoji-menu').removeClass('is-visible');
-    };
-
-    AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) {
-      var $emojiButton, counter;
-      if (checkForMutuality == null) {
-        checkForMutuality = true;
-      }
-      if (checkForMutuality) {
-        this.checkMutuality(votesBlock, emoji);
-      }
-      this.addEmojiToFrequentlyUsedList(emoji);
-      emoji = this.normilizeEmojiName(emoji);
-      $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent();
-      if ($emojiButton.length > 0) {
-        if (this.isActive($emojiButton)) {
-          return this.decrementCounter($emojiButton, emoji);
-        } else {
-          counter = $emojiButton.find('.js-counter');
-          counter.text(parseInt(counter.text(), 10) + 1);
-          $emojiButton.addClass('active');
-          this.addYouToUserList(votesBlock, emoji);
-          return this.animateEmoji($emojiButton);
-        }
-      } else {
-        votesBlock.removeClass('hidden');
-        return this.createEmoji(votesBlock, emoji);
-      }
-    };
-
-    AwardsHandler.prototype.getVotesBlock = function() {
-      var currentBlock;
-      currentBlock = $('.js-awards-block.current');
-      if (currentBlock.length) {
-        return currentBlock;
-      } else {
-        return $('.js-awards-block').eq(0);
-      }
-    };
-
-    AwardsHandler.prototype.getAwardUrl = function() {
-      return this.getVotesBlock().data('award-url');
-    };
-
-    AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) {
-      var $emojiButton, awardUrl, isAlreadyVoted, mutualVote;
-      awardUrl = this.getAwardUrl();
-      if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
-        mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
-        $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent();
-        isAlreadyVoted = $emojiButton.hasClass('active');
-        if (isAlreadyVoted) {
-          this.addAward(votesBlock, awardUrl, mutualVote, false);
-        }
-      }
-    };
-
-    AwardsHandler.prototype.isActive = function($emojiButton) {
-      return $emojiButton.hasClass('active');
-    };
-
-    AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) {
-      var counter, counterNumber;
-      counter = $('.js-counter', $emojiButton);
-      counterNumber = parseInt(counter.text(), 10);
-      if (counterNumber > 1) {
-        counter.text(counterNumber - 1);
-        this.removeYouFromUserList($emojiButton, emoji);
-      } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
-        $emojiButton.tooltip('destroy');
-        counter.text('0');
-        this.removeYouFromUserList($emojiButton, emoji);
-        if ($emojiButton.parents('.note').length) {
-          this.removeEmoji($emojiButton);
-        }
-      } else {
-        this.removeEmoji($emojiButton);
-      }
-      return $emojiButton.removeClass('active');
-    };
-
-    AwardsHandler.prototype.removeEmoji = function($emojiButton) {
-      var $votesBlock;
-      $emojiButton.tooltip('destroy');
-      $emojiButton.remove();
-      $votesBlock = this.getVotesBlock();
-      if ($votesBlock.find('.js-emoji-btn').length === 0) {
-        return $votesBlock.addClass('hidden');
-      }
-    };
-
-    AwardsHandler.prototype.getAwardTooltip = function($awardBlock) {
-      return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
-    };
-
-    AwardsHandler.prototype.toSentence = function(list) {
-      if (list.length <= 2) {
-        return list.join(' and ');
-      }
-      else {
-        return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
+    }
+    // Prebuild the categoryMap
+    categoryMap = categoryMap || buildCategoryMap();
+  });
+  this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
+    e.stopPropagation();
+    e.preventDefault();
+    this.showEmojiMenu($(e.currentTarget));
+  });
+
+  this.registerEventListener('on', $('html'), 'click', (e) => {
+    const $target = $(e.target);
+    if (!$target.closest('.emoji-menu-content').length) {
+      $('.js-awards-block.current').removeClass('current');
+    }
+    if (!$target.closest('.emoji-menu').length) {
+      if ($('.emoji-menu').is(':visible')) {
+        $('.js-add-award.is-active').removeClass('is-active');
+        $('.emoji-menu').removeClass('is-visible');
       }
-    };
-
-    AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
-      var authors, awardBlock, newAuthors, originalTitle;
-      awardBlock = $emojiButton;
-      originalTitle = this.getAwardTooltip(awardBlock);
-      authors = originalTitle.split(FROM_SENTENCE_REGEX);
-      authors.splice(authors.indexOf('You'), 1);
-      return awardBlock
-        .closest('.js-emoji-btn')
-        .removeData('title')
-        .removeAttr('data-title')
-        .removeAttr('data-original-title')
-        .attr('title', this.toSentence(authors))
-        .tooltip('fixTitle');
-    };
-
-    AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
-      var awardBlock, origTitle, users;
-      awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
-      origTitle = this.getAwardTooltip(awardBlock);
-      users = [];
-      if (origTitle) {
-        users = origTitle.trim().split(FROM_SENTENCE_REGEX);
+    }
+  });
+  this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
+    e.preventDefault();
+    const $target = $(e.currentTarget);
+    const $glEmojiElement = $target.find('gl-emoji');
+    const $spriteIconElement = $target.find('.icon');
+    const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+    $target.closest('.js-awards-block').addClass('current');
+    return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
+  });
+}
+
+AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) {
+  element[method].call(element, ...args);
+  this.eventListeners.push({
+    element,
+    args,
+  });
+};
+
+AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
+  if ($addBtn.hasClass('js-note-emoji')) {
+    $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+  } else {
+    $addBtn.closest('.js-awards-block').addClass('current');
+  }
+
+  const $menu = $('.emoji-menu');
+  if ($menu.length) {
+    if ($menu.is('.is-visible')) {
+      $addBtn.removeClass('is-active');
+      $menu.removeClass('is-visible');
+      $('#emoji_search').blur();
+    } else {
+      $addBtn.addClass('is-active');
+      this.positionMenu($menu, $addBtn);
+      $menu.addClass('is-visible');
+      $('#emoji_search').focus();
+    }
+  } else {
+    $addBtn.addClass('is-loading is-active');
+    this.createEmojiMenu(() => {
+      const $createdMenu = $('.emoji-menu');
+      $addBtn.removeClass('is-loading');
+      this.positionMenu($createdMenu, $addBtn);
+      return setTimeout(() => {
+        $createdMenu.addClass('is-visible');
+        $('#emoji_search').focus();
+      }, 200);
+    });
+  }
+};
+
+// Create the emoji menu with the first category of emojis.
+// Then render the remaining categories of emojis one by one to avoid jank.
+AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
+  if (this.isCreatingEmojiMenu) {
+    return;
+  }
+  this.isCreatingEmojiMenu = true;
+
+  // Render the first category
+  categoryMap = categoryMap || buildCategoryMap();
+  const categoryNameKey = Object.keys(categoryMap)[0];
+  const emojisInCategory = categoryMap[categoryNameKey];
+  const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
+
+  // Render the frequently used
+  const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
+  let frequentlyUsedCatgegory = '';
+  if (frequentlyUsedEmojis.length > 0) {
+    frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
+      menuListClass: 'frequent-emojis',
+    });
+  }
+
+  const emojiMenuMarkup = `
+    <div class="emoji-menu">
+      <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" />
+
+      <div class="emoji-menu-content">
+        ${frequentlyUsedCatgegory}
+        ${firstCategory}
+      </div>
+    </div>
+  `;
+
+  document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
+
+  this.addRemainingEmojiMenuCategories();
+  this.setupSearch();
+  if (callback) {
+    callback();
+  }
+};
+
+AwardsHandler
+  .prototype
+  .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() {
+    if (this.isAddingRemainingEmojiMenuCategories) {
+      return;
+    }
+    this.isAddingRemainingEmojiMenuCategories = true;
+
+    categoryMap = categoryMap || buildCategoryMap();
+
+    // Avoid the jank and render the remaining categories separately
+    // This will take more time, but makes UI more responsive
+    const menu = document.querySelector('.emoji-menu');
+    const emojiContentElement = menu.querySelector('.emoji-menu-content');
+    const remainingCategories = Object.keys(categoryMap).slice(1);
+    const allCategoriesAddedPromise = remainingCategories.reduce(
+      (promiseChain, categoryNameKey) =>
+        promiseChain.then(() =>
+          new Promise((resolve) => {
+            const emojisInCategory = categoryMap[categoryNameKey];
+            const categoryMarkup = renderCategory(
+              categoryLabelMap[categoryNameKey],
+              emojisInCategory,
+            );
+            requestAnimationFrame(() => {
+              emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
+              resolve();
+            });
+          }),
+      ),
+      Promise.resolve(),
+    );
+
+    allCategoriesAddedPromise.then(() => {
+      // Used for tests
+      // We check for the menu in case it was destroyed in the meantime
+      if (menu) {
+        menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
       }
-      users.unshift('You');
-      return awardBlock
-        .attr('title', this.toSentence(users))
-        .tooltip('fixTitle');
-    };
-
-    AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
-      var $emojiButton, buttonHtml, emojiCssClass;
-      emojiCssClass = this.resolveNameToCssClass(emoji);
-      buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
-      $emojiButton = $(buttonHtml);
-      $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
+    });
+  };
+
+AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) {
+  const position = $addBtn.data('position');
+  // The menu could potentially be off-screen or in a hidden overflow element
+  // So we position the element absolute in the body
+  const css = {
+    top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
+  };
+  if (position === 'right') {
+    css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
+    $menu.addClass('is-aligned-right');
+  } else {
+    css.left = `${$addBtn.offset().left}px`;
+    $menu.removeClass('is-aligned-right');
+  }
+  return $menu.css(css);
+};
+
+AwardsHandler.prototype.addAward = function addAward(
+  votesBlock,
+  awardUrl,
+  emoji,
+  checkMutuality,
+  callback,
+) {
+  const normalizedEmoji = this.normalizeEmojiName(emoji);
+  this.postEmoji(awardUrl, normalizedEmoji, () => {
+    this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
+    return typeof callback === 'function' ? callback() : undefined;
+  });
+  return $('.emoji-menu').removeClass('is-visible');
+};
+
+AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
+  votesBlock,
+  emoji,
+  checkForMutuality,
+) {
+  if (checkForMutuality || checkForMutuality === null) {
+    this.checkMutuality(votesBlock, emoji);
+  }
+  this.addEmojiToFrequentlyUsedList(emoji);
+  const normalizedEmoji = this.normalizeEmojiName(emoji);
+  const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
+  if ($emojiButton.length > 0) {
+    if (this.isActive($emojiButton)) {
+      this.decrementCounter($emojiButton, normalizedEmoji);
+    } else {
+      const counter = $emojiButton.find('.js-counter');
+      counter.text(parseInt(counter.text(), 10) + 1);
+      $emojiButton.addClass('active');
+      this.addYouToUserList(votesBlock, normalizedEmoji);
       this.animateEmoji($emojiButton);
-      $('.award-control').tooltip();
-      return votesBlock.removeClass('current');
-    };
-
-    AwardsHandler.prototype.animateEmoji = function($emoji) {
-      var className = 'pulse animated once short';
-      $emoji.addClass(className);
+    }
+  } else {
+    votesBlock.removeClass('hidden');
+    this.createEmoji(votesBlock, normalizedEmoji);
+  }
+};
+
+AwardsHandler.prototype.getVotesBlock = function getVotesBlock() {
+  const currentBlock = $('.js-awards-block.current');
+  let resultantVotesBlock = currentBlock;
+  if (currentBlock.length === 0) {
+    resultantVotesBlock = $('.js-awards-block').eq(0);
+  }
+
+  return resultantVotesBlock;
+};
+
+AwardsHandler.prototype.getAwardUrl = function getAwardUrl() {
+  return this.getVotesBlock().data('award-url');
+};
+
+AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) {
+  const awardUrl = this.getAwardUrl();
+  if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+    const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
+    const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
+    const isAlreadyVoted = $emojiButton.hasClass('active');
+    if (isAlreadyVoted) {
+      this.addAward(votesBlock, awardUrl, mutualVote, false);
+    }
+  }
+};
+
+AwardsHandler.prototype.isActive = function isActive($emojiButton) {
+  return $emojiButton.hasClass('active');
+};
+
+AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
+  const counter = $('.js-counter', $emojiButton);
+  const counterNumber = parseInt(counter.text(), 10);
+  if (counterNumber > 1) {
+    counter.text(counterNumber - 1);
+    this.removeYouFromUserList($emojiButton);
+  } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+    $emojiButton.tooltip('destroy');
+    counter.text('0');
+    this.removeYouFromUserList($emojiButton);
+    if ($emojiButton.parents('.note').length) {
+      this.removeEmoji($emojiButton);
+    }
+  } else {
+    this.removeEmoji($emojiButton);
+  }
+  return $emojiButton.removeClass('active');
+};
+
+AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) {
+  $emojiButton.tooltip('destroy');
+  $emojiButton.remove();
+  const $votesBlock = this.getVotesBlock();
+  if ($votesBlock.find('.js-emoji-btn').length === 0) {
+    $votesBlock.addClass('hidden');
+  }
+};
+
+AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) {
+  return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
+};
+
+AwardsHandler.prototype.toSentence = function toSentence(list) {
+  let sentence;
+  if (list.length <= 2) {
+    sentence = list.join(' and ');
+  } else {
+    sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
+  }
+
+  return sentence;
+};
+
+AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) {
+  const awardBlock = $emojiButton;
+  const originalTitle = this.getAwardTooltip(awardBlock);
+  const authors = originalTitle.split(FROM_SENTENCE_REGEX);
+  authors.splice(authors.indexOf('You'), 1);
+  return awardBlock
+    .closest('.js-emoji-btn')
+    .removeData('title')
+    .removeAttr('data-title')
+    .removeAttr('data-original-title')
+    .attr('title', this.toSentence(authors))
+    .tooltip('fixTitle');
+};
+
+AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) {
+  const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
+  const origTitle = this.getAwardTooltip(awardBlock);
+  let users = [];
+  if (origTitle) {
+    users = origTitle.trim().split(FROM_SENTENCE_REGEX);
+  }
+  users.unshift('You');
+  return awardBlock
+    .attr('title', this.toSentence(users))
+    .tooltip('fixTitle');
+};
+
+AwardsHandler
+  .prototype
+  .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) {
+    const buttonHtml = `
+      <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
+        ${glEmojiTag(emojiName)}
+        <span class="award-control-text js-counter">1</span>
+      </button>
+    `;
+    const $emojiButton = $(buttonHtml);
+    $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
+    this.animateEmoji($emojiButton);
+    $('.award-control').tooltip();
+    votesBlock.removeClass('current');
+  };
+
+AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) {
+  const className = 'pulse animated once short';
+  $emoji.addClass(className);
+
+  this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
+    $(e.currentTarget).removeClass(className);
+  });
+};
+
+AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
+  if ($('.emoji-menu').length) {
+    this.createAwardButtonForVotesBlock(votesBlock, emoji);
+  }
+  this.createEmojiMenu(() => {
+    this.createAwardButtonForVotesBlock(votesBlock, emoji);
+  });
+};
+
+AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) {
+  return $.post(awardUrl, {
+    name: emoji,
+  }, (data) => {
+    if (data.ok) {
+      callback();
+    }
+  });
+};
+
+AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
+  return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
+};
+
+AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
+  const options = {
+    scrollTop: $('.awards').offset().top - 110,
+  };
+  return $('body, html').animate(options, 200);
+};
+
+AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) {
+  return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
+};
+
+AwardsHandler
+  .prototype
+  .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
+    if (isEmojiNameValid(emoji)) {
+      this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
+      Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
+    }
+  };
 
-      $emoji.on('webkitAnimationEnd animationEnd', function() {
-        $(this).removeClass(className);
-      });
-    };
+AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
+  return this.frequentlyUsedEmojis || (() => {
+    const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
+    this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
+      inputName => isEmojiNameValid(inputName),
+    );
 
-    AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
-      if ($('.emoji-menu').length) {
-        return this.createEmoji_(votesBlock, emoji);
-      }
-      return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) {
-        return function() {
-          return _this.createEmoji_(votesBlock, emoji);
-        };
-      })(this));
-    };
-
-    AwardsHandler.prototype.getAwardMenuUrl = function() {
-      return gon.award_menu_url;
-    };
-
-    AwardsHandler.prototype.resolveNameToCssClass = function(emoji) {
-      var emojiIcon, unicodeName;
-      emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']");
-      if (emojiIcon.length > 0) {
-        unicodeName = emojiIcon.data('unicode-name');
-      } else {
-        // Find by alias
-        unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
-      }
-      return "emoji-" + unicodeName;
-    };
-
-    AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) {
-      return $.post(awardUrl, {
-        name: emoji
-      }, function(data) {
-        if (data.ok) {
-          return callback();
-        }
-      });
-    };
-
-    AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) {
-      return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']");
-    };
-
-    AwardsHandler.prototype.scrollToAwards = function() {
-      var options;
-      options = {
-        scrollTop: $('.awards').offset().top - 110
-      };
-      return $('body, html').animate(options, 200);
-    };
-
-    AwardsHandler.prototype.normilizeEmojiName = function(emoji) {
-      return this.aliases[emoji] || emoji;
-    };
-
-    AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) {
-      var frequentlyUsedEmojis;
-      frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
-      frequentlyUsedEmojis.push(emoji);
-      Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
-    };
-
-    AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
-      var frequentlyUsedEmojis;
-      frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
-      return _.compact(_.uniq(frequentlyUsedEmojis));
-    };
-
-    AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
-      var emoji, frequentlyUsedEmojis, i, len, ul;
-      if (Cookies.get('frequently_used_emojis')) {
-        frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
-        ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
-        for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
-          emoji = frequentlyUsedEmojis[i];
-          $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
-        }
-        $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used'));
-      }
-      return this.frequentEmojiBlockRendered = true;
-    };
-
-    AwardsHandler.prototype.setupSearch = function() {
-      return $('input.emoji-search').on('keyup', (function(_this) {
-        return function(ev) {
-          var found_emojis, h5, term, ul;
-          term = $(ev.target).val();
-          // Clean previous search results
-          $('ul.emoji-menu-search, h5.emoji-search').remove();
-          if (term) {
-            // Generate a search result block
-            h5 = $('<h5 class="emoji-search" />').text('Search results');
-            found_emojis = _this.searchEmojis(term).show();
-            ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
-            $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
-            return $('.emoji-menu-content').append(h5).append(ul);
-          } else {
-            return $('.emoji-menu-content').children().show();
-          }
-        };
-      })(this));
-    };
-
-    AwardsHandler.prototype.searchEmojis = function(term) {
-      return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone();
-    };
-
-    return AwardsHandler;
+    return this.frequentlyUsedEmojis;
   })();
-}).call(window);
+};
+
+AwardsHandler.prototype.setupSearch = function setupSearch() {
+  this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
+    const term = $(e.target).val().trim();
+    // Clean previous search results
+    $('ul.emoji-menu-search, h5.emoji-search').remove();
+    if (term.length > 0) {
+      // Generate a search result block
+      const h5 = $('<h5 class="emoji-search" />').text('Search results');
+      const foundEmojis = this.searchEmojis(term).show();
+      const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
+      $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
+      $('.emoji-menu-content').append(h5).append(ul);
+    } else {
+      $('.emoji-menu-content').children().show();
+    }
+  });
+};
+
+AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
+  const safeTerm = term.toLowerCase();
+
+  const namesMatchingAlias = [];
+  Object.keys(emojiAliases).forEach((alias) => {
+    if (alias.indexOf(safeTerm) >= 0) {
+      namesMatchingAlias.push(emojiAliases[alias]);
+    }
+  });
+  const $matchingElements = namesMatchingAlias.concat(safeTerm)
+    .reduce(
+      ($result, searchTerm) =>
+        $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
+      $([]),
+    );
+  return $matchingElements.closest('li').clone();
+};
+
+AwardsHandler.prototype.destroy = function destroy() {
+  this.eventListeners.forEach((entry) => {
+    entry.element.off.call(entry.element, ...entry.args);
+  });
+  $('.emoji-menu').remove();
+};
+
+export default AwardsHandler;
diff --git a/app/assets/javascripts/behaviors/bind_in_out.js b/app/assets/javascripts/behaviors/bind_in_out.js
new file mode 100644
index 0000000000000000000000000000000000000000..886f127b06bd9c40e2d100c80a5476f14ed87247
--- /dev/null
+++ b/app/assets/javascripts/behaviors/bind_in_out.js
@@ -0,0 +1,47 @@
+class BindInOut {
+  constructor(bindIn, bindOut) {
+    this.in = bindIn;
+    this.out = bindOut;
+
+    this.eventWrapper = {};
+    this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change';
+  }
+
+  addEvents() {
+    this.eventWrapper.updateOut = this.updateOut.bind(this);
+
+    this.in.addEventListener(this.eventType, this.eventWrapper.updateOut);
+
+    return this;
+  }
+
+  updateOut() {
+    this.out.textContent = this.in.value;
+
+    return this;
+  }
+
+  removeEvents() {
+    this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut);
+
+    return this;
+  }
+
+  static initAll() {
+    const ins = document.querySelectorAll('*[data-bind-in]');
+
+    return [].map.call(ins, anIn => BindInOut.init(anIn));
+  }
+
+  static init(anIn, anOut) {
+    const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`);
+
+    if (!out) return null;
+
+    const bindInOut = new BindInOut(anIn, out);
+
+    return bindInOut.addEvents().updateOut();
+  }
+}
+
+export default BindInOut;
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
new file mode 100644
index 0000000000000000000000000000000000000000..19a607309e468095e872fc6c4b1e150536e1e124
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -0,0 +1,116 @@
+import installCustomElements from 'document-register-element';
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
+import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
+
+installCustomElements(window);
+
+const generatedUnicodeSupportMap = getUnicodeSupportMap();
+
+function emojiImageTag(name, src) {
+  return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
+}
+
+function assembleFallbackImageSrc(inputName) {
+  let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+    emojiAliases[inputName] : inputName;
+  let emojiInfo = emojiMap[name];
+  // Fallback to question mark for unknown emojis
+  if (!emojiInfo) {
+    name = 'grey_question';
+    emojiInfo = emojiMap[name];
+  }
+  const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
+
+  return fallbackImageSrc;
+}
+const glEmojiTagDefaults = {
+  sprite: false,
+  forceFallback: false,
+};
+function glEmojiTag(inputName, options) {
+  const opts = Object.assign({}, glEmojiTagDefaults, options);
+  let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+    emojiAliases[inputName] : inputName;
+  let emojiInfo = emojiMap[name];
+  // Fallback to question mark for unknown emojis
+  if (!emojiInfo) {
+    name = 'grey_question';
+    emojiInfo = emojiMap[name];
+  }
+
+  const fallbackImageSrc = assembleFallbackImageSrc(name);
+  const fallbackSpriteClass = `emoji-${name}`;
+
+  const classList = [];
+  if (opts.forceFallback && opts.sprite) {
+    classList.push('emoji-icon');
+    classList.push(fallbackSpriteClass);
+  }
+  const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
+  const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+  let contents = emojiInfo.moji;
+  if (opts.forceFallback && !opts.sprite) {
+    contents = emojiImageTag(name, fallbackImageSrc);
+  }
+
+  return `
+  <gl-emoji
+    ${classAttribute}
+    data-name="${name}"
+    data-fallback-src="${fallbackImageSrc}"
+    ${fallbackSpriteAttribute}
+    data-unicode-version="${emojiInfo.unicodeVersion}"
+  >
+    ${contents}
+  </gl-emoji>
+  `;
+}
+
+function installGlEmojiElement() {
+  const GlEmojiElementProto = Object.create(HTMLElement.prototype);
+  GlEmojiElementProto.createdCallback = function createdCallback() {
+    const emojiUnicode = this.textContent.trim();
+    const {
+      name,
+      unicodeVersion,
+      fallbackSrc,
+      fallbackSpriteClass,
+    } = this.dataset;
+
+    const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
+      this.childNodes,
+      childNode => childNode.nodeType === 3,
+    );
+    const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
+    const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
+
+    if (
+      isEmojiUnicode &&
+      !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
+    ) {
+      // CSS sprite fallback takes precedence over image fallback
+      if (hasCssSpriteFalback) {
+        // IE 11 doesn't like adding multiple at once :(
+        this.classList.add('emoji-icon');
+        this.classList.add(fallbackSpriteClass);
+      } else if (hasImageFallback) {
+        this.innerHTML = emojiImageTag(name, fallbackSrc);
+      } else {
+        const src = assembleFallbackImageSrc(name);
+        this.innerHTML = emojiImageTag(name, src);
+      }
+    }
+  };
+
+  document.registerElement('gl-emoji', {
+    prototype: GlEmojiElementProto,
+  });
+}
+
+export {
+  installGlEmojiElement,
+  glEmojiTag,
+  emojiImageTag,
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
new file mode 100644
index 0000000000000000000000000000000000000000..be4aeb32c467981bfcd2e2aa09cd2a9d72216bdd
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
@@ -0,0 +1,11 @@
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+
+function isEmojiNameValid(inputName) {
+  const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+    emojiAliases[inputName] : inputName;
+
+  return name && emojiMap[name];
+}
+
+export default isEmojiNameValid;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
new file mode 100644
index 0000000000000000000000000000000000000000..5e3c45f7e929860df007e37fa2470fd10bb3638a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -0,0 +1,121 @@
+import spreadString from './spread_string';
+
+// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
+const flagACodePoint = 127462; // parseInt('1F1E6', 16)
+const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
+function isFlagEmoji(emojiUnicode) {
+  const cp = emojiUnicode.codePointAt(0);
+  // Length 4 because flags are made of 2 characters which are surrogate pairs
+  return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
+}
+
+// Chrome <57 renders keycaps oddly
+// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
+// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
+function isKeycapEmoji(emojiUnicode) {
+  return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
+}
+
+// Check for a skin tone variation emoji which aren't always supported
+const tone1 = 127995;// parseInt('1F3FB', 16)
+const tone5 = 127999;// parseInt('1F3FF', 16)
+function isSkinToneComboEmoji(emojiUnicode) {
+  return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+    const cp = char.codePointAt(0);
+    return cp >= tone1 && cp <= tone5;
+  });
+}
+
+// macOS supports most skin tone emoji's but
+// doesn't support the skin tone versions of horse racing
+const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
+function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
+  return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
+    isSkinToneComboEmoji(emojiUnicode);
+}
+
+// Check for `family_*`, `kiss_*`, `couple_*`
+// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
+const zwj = 8205; // parseInt('200D', 16)
+const personStartCodePoint = 128102; // parseInt('1F466', 16)
+const personEndCodePoint = 128105; // parseInt('1F469', 16)
+function isPersonZwjEmoji(emojiUnicode) {
+  let hasPersonEmoji = false;
+  let hasZwj = false;
+  spreadString(emojiUnicode).forEach((character) => {
+    const cp = character.codePointAt(0);
+    if (cp === zwj) {
+      hasZwj = true;
+    } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
+      hasPersonEmoji = true;
+    }
+  });
+
+  return hasPersonEmoji && hasZwj;
+}
+
+// Helper so we don't have to run `isFlagEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
+  const isFlagResult = isFlagEmoji(emojiUnicode);
+  return (
+    (unicodeSupportMap.flag && isFlagResult) ||
+    !isFlagResult
+  );
+}
+
+// Helper so we don't have to run `isSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
+  const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
+  return (
+    (unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
+    !isSkinToneResult
+  );
+}
+
+// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
+  const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
+  return (
+    (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
+    !isHorseRacingSkinToneResult
+  );
+}
+
+// Helper so we don't have to run `isPersonZwjEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
+  const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
+  return (
+    (unicodeSupportMap.personZwj && isPersonZwjResult) ||
+    !isPersonZwjResult
+  );
+}
+
+// Takes in a support map and determines whether
+// the given unicode emoji is supported on the platform.
+//
+// Combines all the edge case tests into a one-stop shop method
+function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
+  const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
+    unicodeSupportMap.meta.chromeVersion < 57;
+
+  // For comments about each scenario, see the comments above each individual respective function
+  return unicodeSupportMap[unicodeVersion] &&
+    !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
+    checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+    checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
+    checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+    checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
+}
+
+export {
+  isEmojiUnicodeSupported,
+  isFlagEmoji,
+  isKeycapEmoji,
+  isSkinToneComboEmoji,
+  isHorceRacingSkinToneComboEmoji,
+  isPersonZwjEmoji,
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
new file mode 100644
index 0000000000000000000000000000000000000000..327764ec6e926e310466707e8af33f8cc9c74f15
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
@@ -0,0 +1,50 @@
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
+function knownCharCodeAt(givenString, index) {
+  const str = `${givenString}`;
+  const end = str.length;
+
+  const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
+  let idx = index;
+  while ((surrogatePairs.exec(str)) != null) {
+    const li = surrogatePairs.lastIndex;
+    if (li - 2 < idx) {
+      idx += 1;
+    } else {
+      break;
+    }
+  }
+
+  if (idx >= end || idx < 0) {
+    return NaN;
+  }
+
+  const code = str.charCodeAt(idx);
+
+  let high;
+  let low;
+  if (code >= 0xD800 && code <= 0xDBFF) {
+    high = code;
+    low = str.charCodeAt(idx + 1);
+    // Go one further, since one of the "characters" is part of a surrogate pair
+    return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
+  }
+  return code;
+}
+
+// See http://stackoverflow.com/a/38901550/796832
+// ES5/PhantomJS compatible version of spreading a string
+//
+// [...'foo'] -> ['f', 'o', 'o']
+// [...'🖐🏿'] -> ['🖐', '🏿']
+function spreadString(str) {
+  const arr = [];
+  let i = 0;
+  while (!isNaN(knownCharCodeAt(str, i))) {
+    const codePoint = knownCharCodeAt(str, i);
+    arr.push(String.fromCodePoint(codePoint));
+    i += 1;
+  }
+  return arr;
+}
+
+export default spreadString;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa522e20c360377574d7bbc410106005fd1b4768
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
@@ -0,0 +1,161 @@
+const unicodeSupportTestMap = {
+  // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
+  // occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
+  // woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
+  // sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
+  // family_mwgb
+  // Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
+  personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
+  // horse_racing_tone5
+  // Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
+  horseRacing: '\u{1F3C7}\u{1F3FF}',
+  // US flag, http://emojipedia.org/flags/
+  flag: '\u{1F1FA}\u{1F1F8}',
+  // http://emojipedia.org/modifiers/
+  skinToneModifier: [
+    // spy_tone5
+    '\u{1F575}\u{1F3FF}',
+    // person_with_ball_tone5
+    '\u{26F9}\u{1F3FF}',
+    // angel_tone5
+    '\u{1F47C}\u{1F3FF}',
+  ],
+  // rofl, http://emojipedia.org/unicode-9.0/
+  '9.0': '\u{1F923}',
+  // metal, http://emojipedia.org/unicode-8.0/
+  '8.0': '\u{1F918}',
+  // spy, http://emojipedia.org/unicode-7.0/
+  '7.0': '\u{1F575}',
+  // expressionless, http://emojipedia.org/unicode-6.1/
+  6.1: '\u{1F611}',
+  // japanese_goblin, http://emojipedia.org/unicode-6.0/
+  '6.0': '\u{1F47A}',
+  // sailboat, http://emojipedia.org/unicode-5.2/
+  5.2: '\u{26F5}',
+  // mahjong, http://emojipedia.org/unicode-5.1/
+  5.1: '\u{1F004}',
+  // gear, http://emojipedia.org/unicode-4.1/
+  4.1: '\u{2699}',
+  // zap, http://emojipedia.org/unicode-4.0/
+  '4.0': '\u{26A1}',
+  // recycle, http://emojipedia.org/unicode-3.2/
+  3.2: '\u{267B}',
+  // information_source, http://emojipedia.org/unicode-3.0/
+  '3.0': '\u{2139}',
+  // heart, http://emojipedia.org/unicode-1.1/
+  1.1: '\u{2764}',
+};
+
+function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
+  // `4 *` because RGBA
+  const indexOffset = 4 * pixelOffset;
+  const hasColor = imageDataArray[indexOffset + 0] ||
+    imageDataArray[indexOffset + 1] ||
+    imageDataArray[indexOffset + 2];
+  const isVisible = imageDataArray[indexOffset + 3];
+  // Check for some sort of color other than black
+  if (hasColor && isVisible) {
+    return true;
+  }
+  return false;
+}
+
+const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
+const isChrome = chromeMatches && chromeMatches.length > 0;
+const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
+
+// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
+// See 32px, https://i.imgur.com/htY6Zym.png
+// See 16px, https://i.imgur.com/FPPsIF8.png
+const fontSize = 16;
+function generateUnicodeSupportMap(testMap) {
+  const testMapKeys = Object.keys(testMap);
+  const numTestEntries = testMapKeys
+    .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+
+  const canvas = document.createElement('canvas');
+  (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
+  const ctx = canvas.getContext('2d');
+  canvas.width = (2 * fontSize);
+  canvas.height = (numTestEntries * fontSize);
+  ctx.fillStyle = '#000000';
+  ctx.textBaseline = 'middle';
+  ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
+  // Write each emoji to the canvas vertically
+  let writeIndex = 0;
+  testMapKeys.forEach((testKey) => {
+    const testEntry = testMap[testKey];
+    [].concat(testEntry).forEach((emojiUnicode) => {
+      ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+      writeIndex += 1;
+    });
+  });
+
+  // Read from the canvas
+  const resultMap = {};
+  let readIndex = 0;
+  testMapKeys.forEach((testKey) => {
+    const testEntry = testMap[testKey];
+    // This needs to be a `reduce` instead of `every` because we need to
+    // keep the `readIndex` in sync from the writes by running all entries
+    const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+      // Sample along the vertical-middle for a couple of characters
+      const imageData = ctx.getImageData(
+          0,
+          (readIndex * fontSize) + (fontSize / 2),
+          2 * fontSize,
+          1,
+        ).data;
+
+      let isValidEmoji = false;
+      for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
+        const isLookingAtFirstChar = currentPixel < fontSize;
+        const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+        // Check for the emoji somewhere along the row
+        if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
+          isValidEmoji = true;
+
+        // Check to see that nothing is rendered next to the first character
+        // to ensure that the ZWJ sequence rendered as one piece
+        } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
+          isValidEmoji = false;
+          break;
+        }
+      }
+
+      readIndex += 1;
+      return isSatisfied && isValidEmoji;
+    }, true);
+
+    resultMap[testKey] = isTestSatisfied;
+  });
+
+  resultMap.meta = {
+    isChrome,
+    chromeVersion,
+  };
+
+  return resultMap;
+}
+
+function getUnicodeSupportMap() {
+  let unicodeSupportMap;
+  const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+  try {
+    unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
+  } catch (err) {
+    // swallow
+  }
+  if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+    unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
+    window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
+    window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+  }
+
+  return unicodeSupportMap;
+}
+
+export {
+  getUnicodeSupportMap,
+  generateUnicodeSupportMap,
+};
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index a7e68ae5cb96fc1f3047b770ae9e57a43f9d3992..626f3503c915bbe6f90b32d9fdaaffa490c81ed1 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -6,7 +6,7 @@
 // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
 // is submitted.
 //
-require('../extensions/jquery');
+import '../commons/bootstrap';
 
 //
 // ### Example Markup
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 6b21695d082f40ee577f8f144ba381321e0b0ee5..eb7143f5b1a2d9860f5f324df9b86f35c9514cd1 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -4,7 +4,7 @@
 // When called on a form with input fields with the `required` attribute, the
 // form's submit button will be disabled until all required fields have values.
 //
-require('../extensions/jquery');
+import '../commons/bootstrap';
 
 //
 // ### Example Markup
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index a7181904ac9bbfeb257425eff92edcc50f8ce5ea..92f3bb3ff529222bfa164b213e4e05616b36acf2 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -22,8 +22,12 @@
     //   %div.js-toggle-content
     //
     $('body').on('click', '.js-toggle-button', function(e) {
-      e.preventDefault();
       toggleContainer($(this).closest('.js-toggle-container'));
+
+      const targetTag = e.target.tagName.toLowerCase();
+      if (targetTag === 'a' || targetTag === 'button') {
+        e.preventDefault();
+      }
     });
 
     // If we're accessing a permalink, ensure it is not inside a
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js
similarity index 100%
rename from app/assets/javascripts/blob/blob_ci_yaml.js.es6
rename to app/assets/javascripts/blob/blob_ci_yaml.js
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js
similarity index 100%
rename from app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
rename to app/assets/javascripts/blob/blob_dockerfile_selector.js
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
similarity index 100%
rename from app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
rename to app/assets/javascripts/blob/blob_dockerfile_selectors.js
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 5f14ff40eee817186e1605cac3d9e9ba2b5f5f32..8f6bf162d6e43a0dedabc48590c4cdddb844b184 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -36,7 +36,7 @@
             this.removeFile(file);
           });
           return this.on('sending', function(file, xhr, formData) {
-            formData.append('target_branch', form.find('.js-target-branch').val());
+            formData.append('target_branch', form.find('input[name="target_branch"]').val());
             formData.append('create_merge_request', form.find('.js-create-merge-request').val());
             formData.append('commit_message', form.find('.js-commit-message').val());
           });
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js
similarity index 100%
rename from app/assets/javascripts/blob/blob_license_selectors.js.es6
rename to app/assets/javascripts/blob/blob_license_selectors.js
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8f68860fbdd4f4f53da53d30ffe30060161a317
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -0,0 +1,35 @@
+const lineNumberRe = /^L[0-9]+/;
+
+const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
+  const hash = gl.utils.getLocationHash();
+  if (hash && lineNumberRe.test(hash)) {
+    const hashUrlString = `#${hash}`;
+
+    [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
+      const baseHref = permalinkButton.getAttribute('data-original-href') || (() => {
+        const href = permalinkButton.getAttribute('href');
+        permalinkButton.setAttribute('data-original-href', href);
+        return href;
+      })();
+      permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
+    });
+  }
+};
+
+function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
+  const updateBlameAndBlobPermalinkCb = () => {
+    // Wait for the hash to update from the LineHighlighter callback
+    setTimeout(() => {
+      updateLineNumbersOnBlobPermalinks(elementsToUpdate);
+    }, 0);
+  };
+
+  blobContentHolder.addEventListener('click', (e) => {
+    if (e.target.matches(lineNumberSelector)) {
+      updateBlameAndBlobPermalinkCb();
+    }
+  });
+  updateBlameAndBlobPermalinkCb();
+}
+
+export default BlobLinePermalinkUpdater;
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..95517f51b1c76ae3f224da94e6d89649d9eae425
--- /dev/null
+++ b/app/assets/javascripts/blob/create_branch_dropdown.js
@@ -0,0 +1,88 @@
+class CreateBranchDropdown {
+  constructor(el, targetBranchDropdown) {
+    this.targetBranchDropdown = targetBranchDropdown;
+    this.el = el;
+    this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
+    this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
+    this.newBranchField = this.el.querySelector('#new_branch_name');
+    this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
+
+    this.newBranchCreateButton.setAttribute('disabled', '');
+
+    this.addBindings();
+    this.cleanupWrapper = this.cleanup.bind(this);
+    document.addEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  cleanup() {
+    this.cleanBindings();
+    document.removeEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  cleanBindings() {
+    this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
+    this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
+    this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
+    this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
+  }
+
+  addBindings() {
+    this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
+    this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
+    this.resetFormWrapper = this.resetForm.bind(this);
+    this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
+    this.createBranchWrapper = this.createBranch.bind(this);
+
+    this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
+    this.dropdownBack.addEventListener('click', this.resetFormWrapper);
+    this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
+    this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
+  }
+
+  handleCancelClick(e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.resetForm();
+    this.dropdownBack.click();
+  }
+
+  handleNewBranchKeydown(e) {
+    const keyCode = e.which;
+    const ENTER_KEYCODE = 13;
+    if (keyCode === ENTER_KEYCODE) {
+      this.createBranch(e);
+    }
+  }
+
+  enableBranchCreateButton() {
+    if (this.newBranchField.value !== '') {
+      this.newBranchCreateButton.removeAttribute('disabled');
+    } else {
+      this.newBranchCreateButton.setAttribute('disabled', '');
+    }
+  }
+
+  resetForm() {
+    this.newBranchField.value = '';
+    this.enableBranchCreateButtonWrapper();
+  }
+
+  createBranch(e) {
+    e.preventDefault();
+
+    if (this.newBranchCreateButton.getAttribute('disabled') === '') {
+      return;
+    }
+    const newBranchName = this.newBranchField.value;
+    this.targetBranchDropdown.setNewBranch(newBranchName);
+    this.resetForm();
+  }
+}
+
+window.gl = window.gl || {};
+gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..216f069ef71cd24e3d3230a20f17c38319db3287
--- /dev/null
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -0,0 +1,152 @@
+/* eslint-disable class-methods-use-this */
+const SELECT_ITEM_MSG = 'Select';
+
+class TargetBranchDropDown {
+  constructor(dropdown) {
+    this.dropdown = dropdown;
+    this.$dropdown = $(dropdown);
+    this.fieldName = this.dropdown.getAttribute('data-field-name');
+    this.form = this.dropdown.closest('form');
+    this.createDropdown();
+  }
+
+  static bootstrap() {
+    const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
+    [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
+  }
+
+  createDropdown() {
+    const self = this;
+    this.$dropdown.glDropdown({
+      selectable: true,
+      filterable: true,
+      search: {
+        fields: ['title'],
+      },
+      data: (term, callback) => $.ajax({
+        url: self.dropdown.getAttribute('data-refs-url'),
+        data: {
+          ref: self.dropdown.getAttribute('data-ref'),
+          show_all: true,
+        },
+        dataType: 'json',
+      }).done(refs => callback(self.dropdownData(refs))),
+      toggleLabel(item, el) {
+        if (el.is('.is-active')) {
+          return item.text;
+        }
+        return SELECT_ITEM_MSG;
+      },
+      clicked(item, el, e) {
+        e.preventDefault();
+        self.onClick.call(self);
+      },
+      fieldName: self.fieldName,
+    });
+    return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
+  }
+
+  onClick() {
+    this.enableSubmit();
+    this.$dropdown.trigger('change.branch');
+  }
+
+  enableSubmit() {
+    const submitBtn = this.form.querySelector('[type="submit"]');
+    if (this.branchInput && this.branchInput.value) {
+      submitBtn.removeAttribute('disabled');
+    } else {
+      submitBtn.setAttribute('disabled', '');
+    }
+  }
+
+  dropdownData(refs) {
+    const branchList = this.dropdownItems(refs);
+    this.cachedRefs = refs;
+    this.addDefaultBranch(branchList);
+    this.addNewBranch(branchList);
+    return { Branches: branchList };
+  }
+
+  dropdownItems(refs) {
+    return refs.map(this.dropdownItem);
+  }
+
+  dropdownItem(ref) {
+    return { id: ref, text: ref, title: ref };
+  }
+
+  addDefaultBranch(branchList) {
+    // when no branch is selected do nothing
+    if (!this.branchInput) {
+      return;
+    }
+
+    const branchInputVal = this.branchInput.value;
+    const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
+
+    if (currentBranchIndex === -1) {
+      this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
+    }
+  }
+
+  addNewBranch(branchList) {
+    if (this.newBranch) {
+      this.unshiftBranch(branchList, this.newBranch);
+    }
+  }
+
+  searchBranch(branchList, branchName) {
+    return _.findIndex(branchList, el => branchName === el.id);
+  }
+
+  unshiftBranch(branchList, branch) {
+    const branchIndex = this.searchBranch(branchList, branch.id);
+
+    if (branchIndex === -1) {
+      branchList.unshift(branch);
+    }
+  }
+
+  setNewBranch(newBranchName) {
+    this.newBranch = this.dropdownItem(newBranchName);
+    this.refreshData();
+    this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
+  }
+
+  refreshData() {
+    this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
+    this.clearFilter();
+  }
+
+  clearFilter() {
+    // apply an empty filter in order to refresh the data
+    this.glDropdown.filter.filter('');
+    this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
+  }
+
+  selectBranch(index) {
+    const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
+
+    if (!branch.classList.contains('is-active')) {
+      branch.click();
+    } else {
+      this.closeDropdown();
+    }
+  }
+
+  closeDropdown() {
+    this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
+  }
+
+  get branchInput() {
+    return this.form.querySelector(`input[name="${this.fieldName}"]`);
+  }
+
+  get glDropdown() {
+    return this.$dropdown.data('glDropdown');
+  }
+}
+
+window.gl = window.gl || {};
+gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js
similarity index 100%
rename from app/assets/javascripts/blob/template_selector.js.es6
rename to app/assets/javascripts/blob/template_selector.js
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js
similarity index 59%
rename from app/assets/javascripts/boards/boards_bundle.js.es6
rename to app/assets/javascripts/boards/boards_bundle.js
index 878ad1b6031eb469cc68fa9a67fcdb9e7b1f013c..3874c2819a592dc7f0bff92d1a9e3ae5780a5835 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,16 +1,23 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
 /* global Vue */
 /* global BoardService */
 
-function requireAll(context) { return context.keys().map(context); }
+import FilteredSearchBoards from './filtered_search_boards';
+import eventHub from './eventhub';
 
 window.Vue = require('vue');
 window.Vue.use(require('vue-resource'));
-requireAll(require.context('./models',   true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores',   true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins',   true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./filters',  true, /^\.\/.*\.(js|es6)$/));
+require('./models/issue');
+require('./models/label');
+require('./models/list');
+require('./models/milestone');
+require('./models/user');
+require('./stores/boards_store');
+require('./stores/modal_store');
+require('./services/board_service');
+require('./mixins/modal_mixins');
+require('./mixins/sortable_default_options');
+require('./filters/due_date_filters');
 require('./components/board');
 require('./components/board_sidebar');
 require('./components/new_list_dropdown');
@@ -55,6 +62,14 @@ $(() => {
     },
     created () {
       gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
+
+      this.filterManager = new FilteredSearchBoards(Store.filter, true);
+
+      // Listen for updateTokens event
+      eventHub.$on('updateTokens', this.updateTokens);
+    },
+    beforeDestroy() {
+      eventHub.$off('updateTokens', this.updateTokens);
     },
     mounted () {
       Store.disabled = this.disabled;
@@ -73,11 +88,16 @@ $(() => {
           Store.addBlankState();
           this.loading = false;
         });
-    }
+    },
+    methods: {
+      updateTokens() {
+        this.filterManager.updateTokens();
+      }
+    },
   });
 
   gl.IssueBoardsSearch = new Vue({
-    el: document.getElementById('js-boards-search'),
+    el: document.getElementById('js-add-list'),
     data: {
       filters: Store.state.filters
     },
@@ -93,17 +113,53 @@ $(() => {
       modal: ModalStore.store,
       store: Store.state,
     },
+    watch: {
+      disabled() {
+        this.updateTooltip();
+      },
+    },
     computed: {
       disabled() {
         return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
       },
+      tooltipTitle() {
+        if (this.disabled) {
+          return 'Please add a list to your board first';
+        }
+
+        return '';
+      },
+    },
+    methods: {
+      updateTooltip() {
+        const $tooltip = $(this.$el);
+
+        this.$nextTick(() => {
+          if (this.disabled) {
+            $tooltip.tooltip();
+          } else {
+            $tooltip.tooltip('destroy');
+          }
+        });
+      },
+      openModal() {
+        if (!this.disabled) {
+          this.toggleModal(true);
+        }
+      },
+    },
+    mounted() {
+      this.updateTooltip();
     },
     template: `
       <button
-        class="btn btn-create pull-right prepend-left-10 has-tooltip"
+        class="btn btn-create pull-right prepend-left-10"
         type="button"
-        :disabled="disabled"
-        @click="toggleModal(true)">
+        data-placement="bottom"
+        :class="{ 'disabled': disabled }"
+        :title="tooltipTitle"
+        :aria-disabled="disabled"
+        @click="openModal">
         Add issues
       </button>
     `,
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js
similarity index 94%
rename from app/assets/javascripts/boards/components/board.js.es6
rename to app/assets/javascripts/boards/components/board.js
index 18324de18b3d1e476bcb72f28f4de2f2f949fba0..67c0c419713b977fb69cb54507f9bcbaa0f0510a 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js
@@ -2,7 +2,8 @@
 /* global Vue */
 /* global Sortable */
 
-require('./board_blank_state');
+import boardBlankState from './board_blank_state';
+
 require('./board_delete');
 require('./board_list');
 
@@ -17,7 +18,7 @@ require('./board_list');
     components: {
       'board-list': gl.issueBoards.BoardList,
       'board-delete': gl.issueBoards.BoardDelete,
-      'board-blank-state': gl.issueBoards.BoardBlankState
+      boardBlankState,
     },
     props: {
       list: Object,
@@ -28,16 +29,16 @@ require('./board_list');
     data () {
       return {
         detailIssue: Store.detail,
-        filters: Store.state.filters,
+        filter: Store.filter,
       };
     },
     watch: {
-      filters: {
-        handler () {
+      filter: {
+        handler() {
           this.list.page = 1;
           this.list.getIssues(true);
         },
-        deep: true
+        deep: true,
       },
       detailIssue: {
         handler () {
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
new file mode 100644
index 0000000000000000000000000000000000000000..52893d4642b9d656f2c3428c49c18b552023ff0f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -0,0 +1,84 @@
+/* global ListLabel */
+/* global Cookies */
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+  template: `
+    <div class="board-blank-state">
+      <p>
+        Add the following default lists to your Issue Board with one click:
+      </p>
+      <ul class="board-blank-state-list">
+        <li v-for="label in predefinedLabels">
+          <span
+            class="label-color"
+            :style="{ backgroundColor: label.color }">
+          </span>
+          {{ label.title }}
+        </li>
+      </ul>
+      <p>
+        Starting out with the default set of lists will get you right on the way to making the most of your board.
+      </p>
+      <button
+        class="btn btn-create btn-inverted btn-block"
+        type="button"
+        @click.stop="addDefaultLists">
+        Add default lists
+      </button>
+      <button
+        class="btn btn-default btn-block"
+        type="button"
+        @click.stop="clearBlankState">
+        Nevermind, I'll use my own
+      </button>
+    </div>
+  `,
+  data() {
+    return {
+      predefinedLabels: [
+        new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+        new ListLabel({ title: 'Doing', color: '#5CB85C' }),
+      ],
+    };
+  },
+  methods: {
+    addDefaultLists() {
+      this.clearBlankState();
+
+      this.predefinedLabels.forEach((label, i) => {
+        Store.addList({
+          title: label.title,
+          position: i,
+          list_type: 'label',
+          label: {
+            title: label.title,
+            color: label.color,
+          },
+        });
+      });
+
+      Store.state.lists = _.sortBy(Store.state.lists, 'position');
+
+      // Save the labels
+      gl.boardService.generateDefaultLists()
+        .then((resp) => {
+          resp.json().forEach((listObj) => {
+            const list = Store.findList('title', listObj.title);
+
+            list.id = listObj.id;
+            list.label.id = listObj.label.id;
+            list.getIssues();
+          });
+        })
+        .catch(() => {
+          Store.removeList(undefined, 'label');
+          Cookies.remove('issue_board_welcome_hidden', {
+            path: '',
+          });
+          Store.addBlankState();
+        });
+    },
+    clearBlankState: Store.removeBlankState.bind(Store),
+  },
+};
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
deleted file mode 100644
index d76314c1892b5e3a7bcf06012dda6ad50b651a8d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable space-before-function-paren, comma-dangle */
-/* global Vue */
-/* global ListLabel */
-
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
-
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
-
-  gl.issueBoards.BoardBlankState = Vue.extend({
-    data () {
-      return {
-        predefinedLabels: [
-          new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
-          new ListLabel({ title: 'Doing', color: '#5CB85C' })
-        ]
-      };
-    },
-    methods: {
-      addDefaultLists () {
-        this.clearBlankState();
-
-        this.predefinedLabels.forEach((label, i) => {
-          Store.addList({
-            title: label.title,
-            position: i,
-            list_type: 'label',
-            label: {
-              title: label.title,
-              color: label.color
-            }
-          });
-        });
-
-        Store.state.lists = _.sortBy(Store.state.lists, 'position');
-
-        // Save the labels
-        gl.boardService.generateDefaultLists()
-          .then((resp) => {
-            resp.json().forEach((listObj) => {
-              const list = Store.findList('title', listObj.title);
-
-              list.id = listObj.id;
-              list.label.id = listObj.label.id;
-              list.getIssues();
-            });
-          });
-      },
-      clearBlankState: Store.removeBlankState.bind(Store)
-    }
-  });
-})();
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b72090df3186cda46a33aad18b5ee1d54a997e3
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -0,0 +1,70 @@
+/* global Vue */
+require('./issue_card_inner');
+
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+  name: 'BoardsIssueCard',
+  template: `
+    <li class="card"
+      :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+      :index="index"
+      :data-issue-id="issue.id"
+      @mousedown="mouseDown"
+      @mousemove="mouseMove"
+      @mouseup="showIssue($event)">
+      <issue-card-inner
+        :list="list"
+        :issue="issue"
+        :issue-link-base="issueLinkBase"
+        :root-path="rootPath"
+        :update-filters="true" />
+    </li>
+  `,
+  components: {
+    'issue-card-inner': gl.issueBoards.IssueCardInner,
+  },
+  props: {
+    list: Object,
+    issue: Object,
+    issueLinkBase: String,
+    disabled: Boolean,
+    index: Number,
+    rootPath: String,
+  },
+  data() {
+    return {
+      showDetail: false,
+      detailIssue: Store.detail,
+    };
+  },
+  computed: {
+    issueDetailVisible() {
+      return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+    },
+  },
+  methods: {
+    mouseDown() {
+      this.showDetail = true;
+    },
+    mouseMove() {
+      this.showDetail = false;
+    },
+    showIssue(e) {
+      const targetTagName = e.target.tagName.toLowerCase();
+
+      if (targetTagName === 'a' || targetTagName === 'button') return;
+
+      if (this.showDetail) {
+        this.showDetail = false;
+
+        if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+          Store.detail.issue = {};
+        } else {
+          Store.detail.issue = this.issue;
+          Store.detail.list = this.list;
+        }
+      }
+    },
+  },
+};
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
deleted file mode 100644
index 0ea66bd027c70aa7af8efd6ab406744e9432a803..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
-/* global Vue */
-
-require('./issue_card_inner');
-
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
-
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
-
-  gl.issueBoards.BoardCard = Vue.extend({
-    template: '#js-board-list-card',
-    components: {
-      'issue-card-inner': gl.issueBoards.IssueCardInner,
-    },
-    props: {
-      list: Object,
-      issue: Object,
-      issueLinkBase: String,
-      disabled: Boolean,
-      index: Number,
-      rootPath: String,
-    },
-    data () {
-      return {
-        showDetail: false,
-        detailIssue: Store.detail
-      };
-    },
-    computed: {
-      issueDetailVisible () {
-        return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
-      }
-    },
-    methods: {
-      mouseDown () {
-        this.showDetail = true;
-      },
-      mouseMove() {
-        this.showDetail = false;
-      },
-      showIssue (e) {
-        const targetTagName = e.target.tagName.toLowerCase();
-
-        if (targetTagName === 'a' || targetTagName === 'button') return;
-
-        if (this.showDetail) {
-          this.showDetail = false;
-
-          if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
-            Store.detail.issue = {};
-          } else {
-            Store.detail.issue = this.issue;
-            Store.detail.list = this.list;
-          }
-        }
-      }
-    }
-  });
-})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js
similarity index 100%
rename from app/assets/javascripts/boards/components/board_delete.js.es6
rename to app/assets/javascripts/boards/components/board_delete.js
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js
similarity index 78%
rename from app/assets/javascripts/boards/components/board_list.js.es6
rename to app/assets/javascripts/boards/components/board_list.js
index 60b0a30af3f6e0ff3b203708e9ea005f3bd466fa..1330d4ae8402b885a29148134fb9c6e0ac639270 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -2,8 +2,8 @@
 /* global Vue */
 /* global Sortable */
 
-require('./board_card');
-require('./board_new_issue');
+import boardNewIssue from './board_new_issue';
+import boardCard from './board_card';
 
 (() => {
   const Store = gl.issueBoards.BoardsStore;
@@ -14,8 +14,8 @@ require('./board_new_issue');
   gl.issueBoards.BoardList = Vue.extend({
     template: '#js-board-list-template',
     components: {
-      'board-card': gl.issueBoards.BoardCard,
-      'board-new-issue': gl.issueBoards.BoardNewIssue
+      boardCard,
+      boardNewIssue,
     },
     props: {
       disabled: Boolean,
@@ -56,11 +56,6 @@ require('./board_new_issue');
         });
       }
     },
-    computed: {
-      orderedIssues () {
-        return _.sortBy(this.issues, 'priority');
-      },
-    },
     methods: {
       listHeight () {
         return this.$refs.list.getBoundingClientRect().height;
@@ -81,14 +76,20 @@ require('./board_new_issue');
           });
         }
       },
+      toggleForm() {
+        this.showIssueForm = !this.showIssueForm;
+      },
+    },
+    created() {
+      gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
     },
     mounted () {
       const options = gl.issueBoards.getBoardSortableDefaultOptions({
         scroll: document.querySelectorAll('.boards-list')[0],
         group: 'issues',
-        sort: false,
         disabled: this.disabled,
         filter: '.board-list-count, .is-disabled',
+        dataIdAttr: 'data-issue-id',
         onStart: (e) => {
           const card = this.$refs.issue[e.oldIndex];
 
@@ -105,6 +106,13 @@ require('./board_new_issue');
             e.item.remove();
           });
         },
+        onUpdate: (e) => {
+          const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
+          gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
+        },
+        onMove(e) {
+          return !e.related.classList.contains('board-list-count');
+        }
       });
 
       this.sortable = Sortable.create(this.$refs.list, options);
@@ -115,6 +123,9 @@ require('./board_new_issue');
           this.loadNextPage();
         }
       };
-    }
+    },
+    beforeDestroy() {
+      gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
+    },
   });
 })();
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
new file mode 100644
index 0000000000000000000000000000000000000000..b88f59dd6d413961cff424d4cac881978670d55c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -0,0 +1,92 @@
+/* global ListIssue */
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+  name: 'BoardNewIssue',
+  props: {
+    list: Object,
+  },
+  data() {
+    return {
+      title: '',
+      error: false,
+    };
+  },
+  methods: {
+    submit(e) {
+      e.preventDefault();
+      if (this.title.trim() === '') return;
+
+      this.error = false;
+
+      const labels = this.list.label ? [this.list.label] : [];
+      const issue = new ListIssue({
+        title: this.title,
+        labels,
+        subscribed: true,
+      });
+
+      this.list.newIssue(issue)
+        .then(() => {
+          // Need this because our jQuery very kindly disables buttons on ALL form submissions
+          $(this.$refs.submitButton).enable();
+
+          Store.detail.issue = issue;
+          Store.detail.list = this.list;
+        })
+        .catch(() => {
+          // Need this because our jQuery very kindly disables buttons on ALL form submissions
+          $(this.$refs.submitButton).enable();
+
+          // Remove the issue
+          this.list.removeIssue(issue);
+
+          // Show error message
+          this.error = true;
+        });
+
+      this.cancel();
+    },
+    cancel() {
+      this.title = '';
+      gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
+    },
+  },
+  mounted() {
+    this.$refs.input.focus();
+  },
+  template: `
+    <div class="card board-new-issue-form">
+      <form @submit="submit($event)">
+        <div class="flash-container"
+          v-if="error">
+          <div class="flash-alert">
+            An error occured. Please try again.
+          </div>
+        </div>
+        <label class="label-light"
+          :for="list.id + '-title'">
+          Title
+        </label>
+        <input class="form-control"
+          type="text"
+          v-model="title"
+          ref="input"
+          :id="list.id + '-title'" />
+        <div class="clearfix prepend-top-10">
+          <button class="btn btn-success pull-left"
+            type="submit"
+            :disabled="title === ''"
+            ref="submit-button">
+            Submit issue
+          </button>
+          <button class="btn btn-default pull-right"
+            type="button"
+            @click="cancel">
+            Cancel
+          </button>
+        </div>
+      </form>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
deleted file mode 100644
index b5c14a198bafab6cec99305289e340fee35d96d2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable comma-dangle, no-unused-vars */
-/* global Vue */
-/* global ListIssue */
-
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
-
-  window.gl = window.gl || {};
-
-  gl.issueBoards.BoardNewIssue = Vue.extend({
-    props: {
-      list: Object,
-    },
-    data() {
-      return {
-        title: '',
-        error: false
-      };
-    },
-    methods: {
-      submit(e) {
-        e.preventDefault();
-        if (this.title.trim() === '') return;
-
-        this.error = false;
-
-        const labels = this.list.label ? [this.list.label] : [];
-        const issue = new ListIssue({
-          title: this.title,
-          labels,
-          subscribed: true
-        });
-
-        this.list.newIssue(issue)
-          .then((data) => {
-            // Need this because our jQuery very kindly disables buttons on ALL form submissions
-            $(this.$refs.submitButton).enable();
-
-            Store.detail.issue = issue;
-            Store.detail.list = this.list;
-          })
-          .catch(() => {
-            // Need this because our jQuery very kindly disables buttons on ALL form submissions
-            $(this.$refs.submitButton).enable();
-
-            // Remove the issue
-            this.list.removeIssue(issue);
-
-            // Show error message
-            this.error = true;
-          });
-
-        this.cancel();
-      },
-      cancel() {
-        this.title = '';
-        this.$parent.showIssueForm = false;
-      }
-    },
-    mounted() {
-      this.$refs.input.focus();
-    },
-  });
-})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js
similarity index 100%
rename from app/assets/javascripts/boards/components/board_sidebar.js.es6
rename to app/assets/javascripts/boards/components/board_sidebar.js
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js
similarity index 73%
rename from app/assets/javascripts/boards/components/issue_card_inner.js.es6
rename to app/assets/javascripts/boards/components/issue_card_inner.js
index 22a8b971ff8d9739394bf99f1d429100ef4a7df5..69e30cec4c599a0fe1b6dcd082d95e04e2d1d45c 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,4 +1,6 @@
 /* global Vue */
+import eventHub from '../eventhub';
+
 (() => {
   const Store = gl.issueBoards.BoardsStore;
 
@@ -23,6 +25,11 @@
         type: String,
         required: true,
       },
+      updateFilters: {
+        type: Boolean,
+        required: false,
+        default: false,
+      },
     },
     methods: {
       showLabel(label) {
@@ -31,29 +38,25 @@
         return !this.list.label || label.id !== this.list.label.id;
       },
       filterByLabel(label, e) {
-        let labelToggleText = label.title;
-        const labelIndex = Store.state.filters.label_name.indexOf(label.title);
+        if (!this.updateFilters) return;
+
+        const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+        const labelTitle = encodeURIComponent(label.title);
+        const param = `label_name[]=${labelTitle}`;
+        const labelIndex = filterPath.indexOf(param);
         $(e.currentTarget).tooltip('hide');
 
         if (labelIndex === -1) {
-          Store.state.filters.label_name.push(label.title);
-          $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+          filterPath.push(param);
         } else {
-          Store.state.filters.label_name.splice(labelIndex, 1);
-          labelToggleText = Store.state.filters.label_name[0];
-          $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+          filterPath.splice(labelIndex, 1);
         }
 
-        const selectedLabels = Store.state.filters.label_name;
-        if (selectedLabels.length === 0) {
-          labelToggleText = 'Label';
-        } else if (selectedLabels.length > 1) {
-          labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
-        }
-
-        $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+        gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
 
         Store.updateFiltersUrl();
+
+        eventHub.$emit('updateTokens');
       },
       labelStyle(label) {
         return {
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js
similarity index 94%
rename from app/assets/javascripts/boards/components/modal/empty_state.js.es6
rename to app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9300eb92653a0819bd26c5e67bcd32..e6973c3fd599e1659756d7ac119061a7093a223c 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -30,7 +30,7 @@
         if (this.activeTab === 'selected') {
           obj.title = 'You haven\'t selected any issues yet';
           obj.content = `
-            Go back to <strong>All issues</strong> and select some issues
+            Go back to <strong>Open issues</strong> and select some issues
             to add to your board.
           `;
         }
@@ -59,7 +59,7 @@
                 class="btn btn-default"
                 @click="changeTab('all')"
                 v-if="activeTab === 'selected'">
-                All issues
+                Open issues
               </button>
             </div>
           </div>
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd394a2318c6e3ac60c1cb323819c560cb8783a5
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -0,0 +1,24 @@
+import FilteredSearchBoards from '../../filtered_search_boards';
+import FilteredSearchContainer from '../../../filtered_search/container';
+
+export default {
+  name: 'modal-filters',
+  props: {
+    store: {
+      type: Object,
+      required: true,
+    },
+  },
+  mounted() {
+    FilteredSearchContainer.container = this.$el;
+
+    this.filteredSearch = new FilteredSearchBoards(this.store);
+    this.filteredSearch.removeTokens();
+  },
+  beforeDestroy() {
+    this.filteredSearch.cleanup();
+    FilteredSearchContainer.container = document;
+    this.store.path = '';
+  },
+  template: '#js-board-modal-filter',
+};
diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6
deleted file mode 100644
index 6de06811d94482a8a7bb8f0f84263567303ce634..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/modal/filters.js.es6
+++ /dev/null
@@ -1,49 +0,0 @@
-/* global Vue */
-const userFilter = require('./filters/user');
-const milestoneFilter = require('./filters/milestone');
-const labelFilter = require('./filters/label');
-
-module.exports = Vue.extend({
-  name: 'modal-filters',
-  props: {
-    projectId: {
-      type: Number,
-      required: true,
-    },
-    milestonePath: {
-      type: String,
-      required: true,
-    },
-    labelPath: {
-      type: String,
-      required: true,
-    },
-  },
-  destroyed() {
-    gl.issueBoards.ModalStore.setDefaultFilter();
-  },
-  components: {
-    userFilter,
-    milestoneFilter,
-    labelFilter,
-  },
-  template: `
-    <div class="modal-filters">
-      <user-filter
-        dropdown-class-name="dropdown-menu-author"
-        toggle-class-name="js-user-search js-author-search"
-        toggle-label="Author"
-        field-name="author_id"
-        :project-id="projectId"></user-filter>
-      <user-filter
-        dropdown-class-name="dropdown-menu-author"
-        toggle-class-name="js-assignee-search"
-        toggle-label="Assignee"
-        field-name="assignee_id"
-        :null-user="true"
-        :project-id="projectId"></user-filter>
-      <milestone-filter :milestone-path="milestonePath"></milestone-filter>
-      <label-filter :label-path="labelPath"></label-filter>
-    </div>
-  `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
deleted file mode 100644
index 4fc8f72a145673c8423f1ea032371a5af2c87adb..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global LabelsSelect */
-module.exports = Vue.extend({
-  name: 'filter-label',
-  props: {
-    labelPath: {
-      type: String,
-      required: true,
-    },
-  },
-  mounted() {
-    new LabelsSelect(this.$refs.dropdown);
-  },
-  template: `
-    <div class="dropdown">
-      <button
-        class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
-        type="button"
-        data-toggle="dropdown"
-        data-show-any="true"
-        data-show-no="true"
-        :data-labels="labelPath"
-        ref="dropdown">
-        <span class="dropdown-toggle-text">
-          Label
-        </span>
-        <i class="fa fa-chevron-down"></i>
-      </button>
-      <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
-        <div class="dropdown-title">
-          Filter by label
-          <button
-            class="dropdown-title-button dropdown-menu-close"
-            aria-label="Close"
-            type="button">
-            <i class="fa fa-times dropdown-menu-close-icon"></i>
-          </button>
-        </div>
-        <div class="dropdown-input">
-          <input
-            type="search"
-            class="dropdown-input-field"
-            placeholder="Search"
-            autocomplete="off" />
-          <i class="fa fa-search dropdown-input-search"></i>
-          <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
-        </div>
-        <div class="dropdown-content"></div>
-        <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
-      </div>
-    </div>
-  `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
deleted file mode 100644
index d555599d300684307ddba009165d18ab5edb66fd..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
+++ /dev/null
@@ -1,55 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global MilestoneSelect */
-module.exports = Vue.extend({
-  name: 'filter-milestone',
-  props: {
-    milestonePath: {
-      type: String,
-      required: true,
-    },
-  },
-  mounted() {
-    new MilestoneSelect(null, this.$refs.dropdown);
-  },
-  template: `
-    <div class="dropdown">
-      <button
-        class="dropdown-menu-toggle js-milestone-select"
-        type="button"
-        data-toggle="dropdown"
-        data-show-any="true"
-        data-show-upcoming="true"
-        data-field-name="milestone_title"
-        :data-milestones="milestonePath"
-        ref="dropdown">
-        <span class="dropdown-toggle-text">
-          Milestone
-        </span>
-        <i class="fa fa-chevron-down"></i>
-      </button>
-      <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
-        <div class="dropdown-title">
-          <span>Filter by milestone</span>
-          <button
-            class="dropdown-title-button dropdown-menu-close"
-            aria-label="Close"
-            type="button">
-            <i class="fa fa-times dropdown-menu-close-icon"></i>
-          </button>
-        </div>
-        <div class="dropdown-input">
-          <input
-            type="search"
-            class="dropdown-input-field"
-            placeholder="Search milestones"
-            autocomplete="off" />
-          <i class="fa fa-search dropdown-input-search"></i>
-          <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
-        </div>
-        <div class="dropdown-content"></div>
-        <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
-      </div>
-    </div>
-  `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
deleted file mode 100644
index 8523028c29c11998cbb3d30ef374f9b2bdceded2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global UsersSelect */
-module.exports = Vue.extend({
-  name: 'filter-user',
-  props: {
-    toggleClassName: {
-      type: String,
-      required: true,
-    },
-    dropdownClassName: {
-      type: String,
-      required: false,
-      default: '',
-    },
-    toggleLabel: {
-      type: String,
-      required: true,
-    },
-    fieldName: {
-      type: String,
-      required: true,
-    },
-    nullUser: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
-    projectId: {
-      type: Number,
-      required: true,
-    },
-  },
-  mounted() {
-    new UsersSelect(null, this.$refs.dropdown);
-  },
-  computed: {
-    currentUsername() {
-      return gon.current_username;
-    },
-    dropdownTitle() {
-      return `Filter by ${this.toggleLabel.toLowerCase()}`;
-    },
-    inputPlaceholder() {
-      return `Search ${this.toggleLabel.toLowerCase()}`;
-    },
-  },
-  template: `
-    <div class="dropdown">
-      <button
-        class="dropdown-menu-toggle js-user-search"
-        :class="toggleClassName"
-        type="button"
-        data-toggle="dropdown"
-        data-current-user="true"
-        :data-any-user="'Any ' + toggleLabel"
-        :data-null-user="nullUser"
-        :data-field-name="fieldName"
-        :data-project-id="projectId"
-        :data-first-user="currentUsername"
-        ref="dropdown">
-        <span class="dropdown-toggle-text">
-          {{ toggleLabel }}
-        </span>
-        <i class="fa fa-chevron-down"></i>
-      </button>
-      <div
-        class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
-        :class="dropdownClassName">
-        <div class="dropdown-title">
-          {{ dropdownTitle }}
-          <button
-            class="dropdown-title-button dropdown-menu-close"
-            aria-label="Close"
-            type="button">
-            <i class="fa fa-times dropdown-menu-close-icon"></i>
-          </button>
-        </div>
-        <div class="dropdown-input">
-          <input
-            type="search"
-            class="dropdown-input-field"
-            autocomplete="off"
-            :placeholder="inputPlaceholder" />
-          <i class="fa fa-search dropdown-input-search"></i>
-          <i
-            role="button"
-            class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
-          </i>
-        </div>
-        <div class="dropdown-content"></div>
-        <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
-      </div>
-    </div>
-  `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js
similarity index 100%
rename from app/assets/javascripts/boards/components/modal/footer.js.es6
rename to app/assets/javascripts/boards/components/modal/footer.js
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js
similarity index 82%
rename from app/assets/javascripts/boards/components/modal/header.js.es6
rename to app/assets/javascripts/boards/components/modal/header.js
index 70c088f905414d195cf9f7be4521cc5ff143ddfd..116e29cd1778882203e133da9b990197f9316a35 100644
--- a/app/assets/javascripts/boards/components/modal/header.js.es6
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -1,6 +1,7 @@
-/* global Vue */
+import Vue from 'vue';
+import modalFilters from './filters';
+
 require('./tabs');
-const modalFilters = require('./filters');
 
 (() => {
   const ModalStore = gl.issueBoards.ModalStore;
@@ -66,16 +67,7 @@ const modalFilters = require('./filters');
         <div
           class="add-issues-search append-bottom-10"
           v-if="showSearch">
-          <modal-filters
-            :project-id="projectId"
-            :milestone-path="milestonePath"
-            :label-path="labelPath">
-          </modal-filters>
-          <input
-            placeholder="Search issues..."
-            class="form-control"
-            type="search"
-            v-model="searchTerm" />
+          <modal-filters :store="filter" />
           <button
             type="button"
             class="btn btn-success btn-inverted prepend-left-10"
diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js
similarity index 91%
rename from app/assets/javascripts/boards/components/modal/index.js.es6
rename to app/assets/javascripts/boards/components/modal/index.js
index f290cd13763d318268d8ce80d07507eae6832448..4240c97617d14e8a7711b337beaf175c19fea5d8 100644
--- a/app/assets/javascripts/boards/components/modal/index.js.es6
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,5 +1,6 @@
 /* global Vue */
 /* global ListIssue */
+import queryData from '../../utils/query_data';
 
 require('./header');
 require('./list');
@@ -47,9 +48,6 @@ require('./empty_state');
       page() {
         this.loadIssues();
       },
-      searchTerm() {
-        this.searchOperation();
-      },
       showAddIssuesModal() {
         if (this.showAddIssuesModal && !this.issues.length) {
           this.loading = true;
@@ -66,25 +64,20 @@ require('./empty_state');
       },
       filter: {
         handler() {
+          this.page = 1;
           this.loadIssues(true);
         },
         deep: true,
       },
     },
     methods: {
-      searchOperation: _.debounce(function searchOperationDebounce() {
-        this.loadIssues(true);
-      }, 500),
       loadIssues(clearIssues = false) {
         if (!this.showAddIssuesModal) return false;
 
-        const queryData = Object.assign({}, this.filter, {
-          search: this.searchTerm,
+        return gl.boardService.getBacklog(queryData(this.filter.path, {
           page: this.page,
           per: this.perPage,
-        });
-
-        return gl.boardService.getBacklog(queryData).then((res) => {
+        })).then((res) => {
           const data = res.json();
 
           if (clearIssues) {
@@ -123,6 +116,9 @@ require('./empty_state');
         return this.activeTab === 'selected' && this.selectedIssues.length === 0;
       },
     },
+    created() {
+      this.page = 1;
+    },
     components: {
       'modal-header': gl.issueBoards.ModalHeader,
       'modal-list': gl.issueBoards.ModalList,
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js
similarity index 100%
rename from app/assets/javascripts/boards/components/modal/list.js.es6
rename to app/assets/javascripts/boards/components/modal/list.js
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
similarity index 100%
rename from app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
rename to app/assets/javascripts/boards/components/modal/lists_dropdown.js
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js
similarity index 97%
rename from app/assets/javascripts/boards/components/modal/tabs.js.es6
rename to app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f350338eba116834fcee8a643788626f71..1cd6ca0ee88952ae9c66d4bc1dd746ec13c7b4fb 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js.es6
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -23,7 +23,7 @@
               href="#"
               role="button"
               @click.prevent="changeTab('all')">
-              All issues
+              Open issues
               <span class="badge">
                 {{ issuesCount }}
               </span>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js
similarity index 100%
rename from app/assets/javascripts/boards/components/new_list_dropdown.js.es6
rename to app/assets/javascripts/boards/components/new_list_dropdown.js
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
similarity index 100%
rename from app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
rename to app/assets/javascripts/boards/components/sidebar/remove_issue.js
diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/boards/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
new file mode 100644
index 0000000000000000000000000000000000000000..101732309ea2bb02dc1988f447c0a599bcf23131
--- /dev/null
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+import FilteredSearchContainer from '../filtered_search/container';
+
+export default class FilteredSearchBoards extends gl.FilteredSearchManager {
+  constructor(store, updateUrl = false) {
+    super('boards');
+
+    this.store = store;
+    this.updateUrl = updateUrl;
+
+    // Issue boards is slightly different, we handle all the requests async
+    // instead or reloading the page, we just re-fire the list ajax requests
+    this.isHandledAsync = true;
+  }
+
+  updateObject(path) {
+    this.store.path = path.substr(1);
+
+    if (this.updateUrl) {
+      gl.issueBoards.BoardsStore.updateFiltersUrl();
+    }
+  }
+
+  removeTokens() {
+    const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
+
+    // Remove all the tokens as they will be replaced by the search manager
+    [].forEach.call(tokens, (el) => {
+      el.parentNode.removeChild(el);
+    });
+  }
+
+  updateTokens() {
+    this.removeTokens();
+
+    this.loadSearchParamsFromURL();
+
+    // Get the placeholder back if search is empty
+    this.filteredSearchInput.dispatchEvent(new Event('input'));
+  }
+}
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js
similarity index 100%
rename from app/assets/javascripts/boards/filters/due_date_filters.js.es6
rename to app/assets/javascripts/boards/filters/due_date_filters.js
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js
similarity index 100%
rename from app/assets/javascripts/boards/mixins/modal_mixins.js.es6
rename to app/assets/javascripts/boards/mixins/modal_mixins.js
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js
similarity index 100%
rename from app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
rename to app/assets/javascripts/boards/mixins/sortable_default_options.js
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js
similarity index 92%
rename from app/assets/javascripts/boards/models/issue.js.es6
rename to app/assets/javascripts/boards/models/issue.js
index 2d0a295ae4d22429b72442a73d9c62d615fec427..ca5e6fa7e9da1a02848e72e0916b21d0b1f0473b 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,6 +15,7 @@ class ListIssue {
     this.labels = [];
     this.selected = false;
     this.assignee = false;
+    this.position = obj.relative_position || Infinity;
 
     if (obj.assignee) {
       this.assignee = new ListUser(obj.assignee);
@@ -27,10 +28,6 @@ class ListIssue {
     obj.labels.forEach((label) => {
       this.labels.push(new ListLabel(label));
     });
-
-    this.priority = this.labels.reduce((max, label) => {
-      return (label.priority < max) ? label.priority : max;
-    }, Infinity);
   }
 
   addLabel (label) {
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js
similarity index 100%
rename from app/assets/javascripts/boards/models/label.js.es6
rename to app/assets/javascripts/boards/models/label.js
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js
similarity index 76%
rename from app/assets/javascripts/boards/models/list.js.es6
rename to app/assets/javascripts/boards/models/list.js
index 5152be56b66c89473f879931e61d0fe72ef3ff62..f18ad2a0fac46822b4c22018a50b3d49f30f2346 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,6 +1,7 @@
 /* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
 /* global ListIssue */
 /* global ListLabel */
+import queryData from '../utils/query_data';
 
 class List {
   constructor (obj) {
@@ -10,7 +11,6 @@ class List {
     this.title = obj.title;
     this.type = obj.list_type;
     this.preset = ['done', 'blank'].indexOf(this.type) > -1;
-    this.filters = gl.issueBoards.BoardsStore.state.filters;
     this.page = 1;
     this.loading = true;
     this.loadingMore = false;
@@ -65,12 +65,9 @@ class List {
   }
 
   getIssues (emptyIssues = true) {
-    const filters = this.filters;
-    const data = { page: this.page };
+    const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
 
-    Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
-
-    if (this.label) {
+    if (this.label && data.label_name) {
       data.label_name = data.label_name.filter(label => label !== this.label.title);
     }
 
@@ -110,9 +107,20 @@ class List {
   }
 
   addIssue (issue, listFrom, newIndex) {
+    let moveBeforeIid = null;
+    let moveAfterIid = null;
+
     if (!this.findIssue(issue.id)) {
       if (newIndex !== undefined) {
         this.issues.splice(newIndex, 0, issue);
+
+        if (this.issues[newIndex - 1]) {
+          moveBeforeIid = this.issues[newIndex - 1].id;
+        }
+
+        if (this.issues[newIndex + 1]) {
+          moveAfterIid = this.issues[newIndex + 1].id;
+        }
       } else {
         this.issues.push(issue);
       }
@@ -123,14 +131,26 @@ class List {
 
       if (listFrom) {
         this.issuesSize += 1;
-        gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
-          .then(() => {
-            listFrom.getIssues(false);
-          });
+
+        this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
       }
     }
   }
 
+  moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
+    this.issues.splice(oldIndex, 1);
+    this.issues.splice(newIndex, 0, issue);
+
+    gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid);
+  }
+
+  updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
+    gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
+      .then(() => {
+        listFrom.getIssues(false);
+      });
+  }
+
   findIssue (id) {
     return this.issues.filter(issue => issue.id === id)[0];
   }
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js
similarity index 100%
rename from app/assets/javascripts/boards/models/milestone.js.es6
rename to app/assets/javascripts/boards/models/milestone.js
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js
similarity index 100%
rename from app/assets/javascripts/boards/models/user.js.es6
rename to app/assets/javascripts/boards/models/user.js
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js
similarity index 92%
rename from app/assets/javascripts/boards/services/board_service.js.es6
rename to app/assets/javascripts/boards/services/board_service.js
index 065e90518dfdcd9fa431f142a817e93844abe455..e54102814d6976dabaadcf451602b07f010aaa9f 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -64,10 +64,12 @@ class BoardService {
     return this.issues.get(data);
   }
 
-  moveIssue (id, from_list_id, to_list_id) {
+  moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
     return this.issue.update({ id }, {
       from_list_id,
-      to_list_id
+      to_list_id,
+      move_before_iid,
+      move_after_iid,
     });
   }
 
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js
similarity index 82%
rename from app/assets/javascripts/boards/stores/boards_store.js.es6
rename to app/assets/javascripts/boards/stores/boards_store.js
index 50842ecbaaa9db50242d28992342cd1404f72c59..28ecb322df775d43df2cffbb6a05f0a308d8c76f 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,9 @@
 
   gl.issueBoards.BoardsStore = {
     disabled: false,
+    filter: {
+      path: '',
+    },
     state: {},
     detail: {
       issue: {}
@@ -18,13 +21,7 @@
     },
     create () {
       this.state.lists = [];
-      this.state.filters = {
-        author_id: gl.utils.getParameterValues('author_id')[0],
-        assignee_id: gl.utils.getParameterValues('assignee_id')[0],
-        milestone_title: gl.utils.getParameterValues('milestone_title')[0],
-        label_name: gl.utils.getParameterValues('label_name[]'),
-        search: ''
-      };
+      this.filter.path = gl.utils.getUrlParamsArray().join('&');
     },
     addList (listObj) {
       const list = new List(listObj);
@@ -92,9 +89,12 @@
       const issueLists = issue.getLists();
       const listLabels = issueLists.map(listIssue => listIssue.label);
 
-      // Add to new lists issues if it doesn't already exist
       if (!issueTo) {
+        // Add to new lists issues if it doesn't already exist
         listTo.addIssue(issue, listFrom, newIndex);
+      } else {
+        listTo.updateIssueLabel(issue, listFrom);
+        issueTo.removeLabel(listFrom.label);
       }
 
       if (listTo.type === 'done') {
@@ -106,6 +106,12 @@
         listFrom.removeIssue(issue);
       }
     },
+    moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+      const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
+      const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+
+      list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
+    },
     findList (key, val, type = 'label') {
       return this.state.lists.filter((list) => {
         const byType = type ? list['type'] === type : true;
@@ -114,7 +120,7 @@
       })[0];
     },
     updateFiltersUrl () {
-      history.pushState(null, null, `?${$.param(this.state.filters)}`);
+      history.pushState(null, null, `?${this.filter.path}`);
     }
   };
 })();
diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js
similarity index 91%
rename from app/assets/javascripts/boards/stores/modal_store.js.es6
rename to app/assets/javascripts/boards/stores/modal_store.js
index 15fc6c79e8d3891d744f235542b1c681a46d7f49..7ee266a831f2b328998677cc4750808965515cfa 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js.es6
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -17,17 +17,9 @@
         loadingNewPage: false,
         page: 1,
         perPage: 50,
-      };
-
-      this.setDefaultFilter();
-    }
-
-    setDefaultFilter() {
-      this.store.filter = {
-        author_id: '',
-        assignee_id: '',
-        milestone_title: '',
-        label_name: [],
+        filter: {
+          path: '',
+        },
       };
     }
 
diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..2cd3c146f1149dcd8048fc0502f3abe6359363a0
--- /dev/null
+++ b/app/assets/javascripts/boards/utils/query_data.js
@@ -0,0 +1,21 @@
+export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
+  if (filterParam === '') return dataParam;
+
+  const data = dataParam;
+  const paramSplit = filterParam.split('=');
+  const paramKeyNormalized = paramSplit[0].replace('[]', '');
+  const isArray = paramSplit[0].indexOf('[]');
+  const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
+
+  if (isArray !== -1) {
+    if (!data[paramKeyNormalized]) {
+      data[paramKeyNormalized] = [];
+    }
+
+    data[paramKeyNormalized].push(value);
+  } else {
+    data[paramKeyNormalized] = value;
+  }
+
+  return data;
+}, extraData);
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 22e9332854840137631b1d872cb2b4b1f3d5aec0..2c1f988d987330ab255c4440898adb879269a1d6 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,72 +1,66 @@
 /* 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, no-return-assign, new-parens, no-param-reassign, max-len */
 
-(function() {
-  var Breakpoints = (function() {
-    var BreakpointInstance, instance;
+var Breakpoints = (function() {
+  var BreakpointInstance, instance;
 
-    function Breakpoints() {}
+  function Breakpoints() {}
 
-    instance = null;
+  instance = null;
 
-    BreakpointInstance = (function() {
-      var BREAKPOINTS;
+  BreakpointInstance = (function() {
+    var BREAKPOINTS;
 
-      BREAKPOINTS = ["xs", "sm", "md", "lg"];
+    BREAKPOINTS = ["xs", "sm", "md", "lg"];
 
-      function BreakpointInstance() {
-        this.setup();
-      }
-
-      BreakpointInstance.prototype.setup = function() {
-        var allDeviceSelector, els;
-        allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
-          return ".device-" + breakpoint;
-        });
-        if ($(allDeviceSelector.join(",")).length) {
-          return;
-        }
-        // Create all the elements
-        els = $.map(BREAKPOINTS, function(breakpoint) {
-          return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
-        });
-        return $("body").append(els.join(''));
-      };
+    function BreakpointInstance() {
+      this.setup();
+    }
 
-      BreakpointInstance.prototype.visibleDevice = function() {
-        var allDeviceSelector;
-        allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
-          return ".device-" + breakpoint;
-        });
-        return $(allDeviceSelector.join(",")).filter(":visible");
-      };
-
-      BreakpointInstance.prototype.getBreakpointSize = function() {
-        var $visibleDevice;
-        $visibleDevice = this.visibleDevice;
-        // TODO: Consider refactoring in light of turbolinks removal.
-        // the page refreshed via turbolinks
-        if (!$visibleDevice().length) {
-          this.setup();
-        }
-        $visibleDevice = this.visibleDevice();
-        return $visibleDevice.attr("class").split("visible-")[1];
-      };
+    BreakpointInstance.prototype.setup = function() {
+      var allDeviceSelector, els;
+      allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+        return ".device-" + breakpoint;
+      });
+      if ($(allDeviceSelector.join(",")).length) {
+        return;
+      }
+      // Create all the elements
+      els = $.map(BREAKPOINTS, function(breakpoint) {
+        return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
+      });
+      return $("body").append(els.join(''));
+    };
 
-      return BreakpointInstance;
-    })();
+    BreakpointInstance.prototype.visibleDevice = function() {
+      var allDeviceSelector;
+      allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+        return ".device-" + breakpoint;
+      });
+      return $(allDeviceSelector.join(",")).filter(":visible");
+    };
 
-    Breakpoints.get = function() {
-      return instance != null ? instance : instance = new BreakpointInstance;
+    BreakpointInstance.prototype.getBreakpointSize = function() {
+      var $visibleDevice;
+      $visibleDevice = this.visibleDevice;
+      // TODO: Consider refactoring in light of turbolinks removal.
+      // the page refreshed via turbolinks
+      if (!$visibleDevice().length) {
+        this.setup();
+      }
+      $visibleDevice = this.visibleDevice();
+      return $visibleDevice.attr("class").split("visible-")[1];
     };
 
-    return Breakpoints;
+    return BreakpointInstance;
   })();
 
-  $((function(_this) {
-    return function() {
-      return _this.bp = Breakpoints.get();
-    };
-  })(this));
+  Breakpoints.get = function() {
+    return instance != null ? instance : instance = new BreakpointInstance;
+  };
+
+  return Breakpoints;
+})();
+
+$(() => { window.bp = Breakpoints.get(); });
 
-  window.Breakpoints = Breakpoints;
-}).call(window);
+window.Breakpoints = Breakpoints;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index e8531c43b4b4883723478ed82338fd6e40a21596..f73e489e7b2795337ba547866d37f9ad0b224641 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,34 +1,33 @@
 /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
-(function() {
-  $(function() {
-    var previewPath;
-    $('input#broadcast_message_color').on('input', function() {
-      var previewColor;
-      previewColor = $(this).val();
-      return $('div.broadcast-message-preview').css('background-color', previewColor);
-    });
-    $('input#broadcast_message_font').on('input', function() {
-      var previewColor;
-      previewColor = $(this).val();
-      return $('div.broadcast-message-preview').css('color', previewColor);
-    });
-    previewPath = $('textarea#broadcast_message_message').data('preview-path');
-    return $('textarea#broadcast_message_message').on('input', function() {
-      var message;
-      message = $(this).val();
-      if (message === '') {
-        return $('.js-broadcast-message-preview').text("Your message here");
-      } else {
-        return $.ajax({
-          url: previewPath,
-          type: "POST",
-          data: {
-            broadcast_message: {
-              message: message
-            }
+
+$(function() {
+  var previewPath;
+  $('input#broadcast_message_color').on('input', function() {
+    var previewColor;
+    previewColor = $(this).val();
+    return $('div.broadcast-message-preview').css('background-color', previewColor);
+  });
+  $('input#broadcast_message_font').on('input', function() {
+    var previewColor;
+    previewColor = $(this).val();
+    return $('div.broadcast-message-preview').css('color', previewColor);
+  });
+  previewPath = $('textarea#broadcast_message_message').data('preview-path');
+  return $('textarea#broadcast_message_message').on('input', function() {
+    var message;
+    message = $(this).val();
+    if (message === '') {
+      return $('.js-broadcast-message-preview').text("Your message here");
+    } else {
+      return $.ajax({
+        url: previewPath,
+        type: "POST",
+        data: {
+          broadcast_message: {
+            message: message
           }
-        });
-      }
-    });
+        }
+      });
+    }
   });
-}).call(window);
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 8fa1aceddffa725e135224d0eed0be2b736ad72e..6efd26ccc3788ab2b290fac0c86315cba7e9be5f 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,278 +1,283 @@
 /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
 /* global Breakpoints */
 
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-  var AUTO_SCROLL_OFFSET = 75;
-  var DOWN_BUILD_TRACE = '#down-build-trace';
-
-  this.Build = (function() {
-    Build.interval = null;
-
-    Build.state = null;
-
-    function Build(options) {
-      options = options || $('.js-build-options').data();
-      this.pageUrl = options.pageUrl;
-      this.buildUrl = options.buildUrl;
-      this.buildStatus = options.buildStatus;
-      this.state = options.logState;
-      this.buildStage = options.buildStage;
-      this.updateDropdown = bind(this.updateDropdown, this);
-      this.$document = $(document);
-      this.$body = $('body');
-      this.$buildTrace = $('#build-trace');
-      this.$autoScrollContainer = $('.autoscroll-container');
-      this.$autoScrollStatus = $('#autoscroll-status');
-      this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
-      this.$upBuildTrace = $('#up-build-trace');
-      this.$downBuildTrace = $(DOWN_BUILD_TRACE);
-      this.$scrollTopBtn = $('#scroll-top');
-      this.$scrollBottomBtn = $('#scroll-bottom');
-      this.$buildRefreshAnimation = $('.js-build-refresh');
-
-      clearInterval(Build.interval);
-      // Init breakpoint checker
-      this.bp = Breakpoints.get();
-
-      this.initSidebar();
-      this.$buildScroll = $('#js-build-scroll');
-
-      this.populateJobs(this.buildStage);
-      this.updateStageDropdownText(this.buildStage);
-      this.sidebarOnResize();
-
-      this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
-      this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
-      this.$document.on('scroll', this.initScrollMonitor.bind(this));
-      $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
-      $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
-      this.updateArtifactRemoveDate();
-      if ($('#build-trace').length) {
-        this.getInitialBuildTrace();
-        this.initScrollButtonAffix();
-      }
-      if (this.buildStatus === "running" || this.buildStatus === "pending") {
-        Build.interval = setInterval((function(_this) {
-          // Check for new build output if user still watching build page
-          // Only valid for runnig build when output changes during time
-          return function() {
-            if (_this.location() === _this.pageUrl) {
-              return _this.getBuildTrace();
-            }
-          };
-        })(this), 4000);
-      }
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+var AUTO_SCROLL_OFFSET = 75;
+var DOWN_BUILD_TRACE = '#down-build-trace';
+
+window.Build = (function() {
+  Build.timeout = null;
+
+  Build.state = null;
+
+  function Build(options) {
+    options = options || $('.js-build-options').data();
+    this.pageUrl = options.pageUrl;
+    this.buildUrl = options.buildUrl;
+    this.buildStatus = options.buildStatus;
+    this.state = options.logState;
+    this.buildStage = options.buildStage;
+    this.updateDropdown = bind(this.updateDropdown, this);
+    this.$document = $(document);
+    this.$body = $('body');
+    this.$buildTrace = $('#build-trace');
+    this.$autoScrollContainer = $('.autoscroll-container');
+    this.$autoScrollStatus = $('#autoscroll-status');
+    this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+    this.$upBuildTrace = $('#up-build-trace');
+    this.$downBuildTrace = $(DOWN_BUILD_TRACE);
+    this.$scrollTopBtn = $('#scroll-top');
+    this.$scrollBottomBtn = $('#scroll-bottom');
+    this.$buildRefreshAnimation = $('.js-build-refresh');
+
+    clearTimeout(Build.timeout);
+    // Init breakpoint checker
+    this.bp = Breakpoints.get();
+
+    this.initSidebar();
+    this.$buildScroll = $('#js-build-scroll');
+
+    this.populateJobs(this.buildStage);
+    this.updateStageDropdownText(this.buildStage);
+    this.sidebarOnResize();
+
+    this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
+    this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+    this.$document.on('scroll', this.initScrollMonitor.bind(this));
+    $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+    $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
+    this.updateArtifactRemoveDate();
+    if ($('#build-trace').length) {
+      this.getInitialBuildTrace();
+      this.initScrollButtonAffix();
     }
-
-    Build.prototype.initSidebar = function() {
-      this.$sidebar = $('.js-build-sidebar');
-      this.$sidebar.niceScroll();
-      this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
-    };
-
-    Build.prototype.location = function() {
-      return window.location.href.split("#")[0];
-    };
-
-    Build.prototype.getInitialBuildTrace = function() {
-      var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
-
-      return $.ajax({
-        url: this.buildUrl,
-        dataType: 'json',
-        success: function(buildData) {
-          $('.js-build-output').html(buildData.trace_html);
-          if (window.location.hash === DOWN_BUILD_TRACE) {
-            $("html,body").scrollTop(this.$buildTrace.height());
+    this.invokeBuildTrace();
+  }
+
+  Build.prototype.initSidebar = function() {
+    this.$sidebar = $('.js-build-sidebar');
+    this.$sidebar.niceScroll();
+    this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+  };
+
+  Build.prototype.location = function() {
+    return window.location.href.split("#")[0];
+  };
+
+  Build.prototype.invokeBuildTrace = function() {
+    var continueRefreshStatuses = ['running', 'pending'];
+    // Continue to update build trace when build is running or pending
+    if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
+      // Check for new build output if user still watching build page
+      // Only valid for runnig build when output changes during time
+      Build.timeout = setTimeout((function(_this) {
+        return function() {
+          if (_this.location() === _this.pageUrl) {
+            return _this.getBuildTrace();
           }
-          if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
-            this.$buildRefreshAnimation.remove();
-            return this.initScrollMonitor();
+        };
+      })(this), 4000);
+    }
+  };
+
+  Build.prototype.getInitialBuildTrace = function() {
+    var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
+
+    return $.ajax({
+      url: this.buildUrl,
+      dataType: 'json',
+      success: function(buildData) {
+        $('.js-build-output').html(buildData.trace_html);
+        if (window.location.hash === DOWN_BUILD_TRACE) {
+          $("html,body").scrollTop(this.$buildTrace.height());
+        }
+        if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
+          this.$buildRefreshAnimation.remove();
+          return this.initScrollMonitor();
+        }
+      }.bind(this)
+    });
+  };
+
+  Build.prototype.getBuildTrace = function() {
+    return $.ajax({
+      url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
+      dataType: "json",
+      success: (function(_this) {
+        return function(log) {
+          var pageUrl;
+
+          if (log.state) {
+            _this.state = log.state;
           }
-        }.bind(this)
-      });
-    };
-
-    Build.prototype.getBuildTrace = function() {
-      return $.ajax({
-        url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
-        dataType: "json",
-        success: (function(_this) {
-          return function(log) {
-            var pageUrl;
-
-            if (log.state) {
-              _this.state = log.state;
+          _this.invokeBuildTrace();
+          if (log.status === "running") {
+            if (log.append) {
+              $('.js-build-output').append(log.html);
+            } else {
+              $('.js-build-output').html(log.html);
             }
-            if (log.status === "running") {
-              if (log.append) {
-                $('.js-build-output').append(log.html);
-              } else {
-                $('.js-build-output').html(log.html);
-              }
-              return _this.checkAutoscroll();
-            } else if (log.status !== _this.buildStatus) {
-              pageUrl = _this.pageUrl;
-              if (_this.$autoScrollStatus.data('state') === 'enabled') {
-                pageUrl += DOWN_BUILD_TRACE;
-              }
-
-              return gl.utils.visitUrl(pageUrl);
+            return _this.checkAutoscroll();
+          } else if (log.status !== _this.buildStatus) {
+            pageUrl = _this.pageUrl;
+            if (_this.$autoScrollStatus.data('state') === 'enabled') {
+              pageUrl += DOWN_BUILD_TRACE;
             }
-          };
-        })(this)
-      });
-    };
-
-    Build.prototype.checkAutoscroll = function() {
-      if (this.$autoScrollStatus.data("state") === "enabled") {
-        return $("html,body").scrollTop(this.$buildTrace.height());
-      }
-
-      // Handle a situation where user started new build
-      // but never scrolled a page
-      if (!this.$scrollTopBtn.is(':visible') &&
-          !this.$scrollBottomBtn.is(':visible') &&
-          !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
-        this.$scrollBottomBtn.show();
-      }
-    };
 
-    Build.prototype.initScrollButtonAffix = function() {
-      // Hide everything initially
-      this.$scrollTopBtn.hide();
-      this.$scrollBottomBtn.hide();
-      this.$autoScrollContainer.hide();
-    };
-
-    // Page scroll listener to detect if user has scrolling page
-    // and handle following cases
-    // 1) User is at Top of Build Log;
-    //      - Hide Top Arrow button
-    //      - Show Bottom Arrow button
-    //      - Disable Autoscroll and hide indicator (when build is running)
-    // 2) User is at Bottom of Build Log;
-    //      - Show Top Arrow button
-    //      - Hide Bottom Arrow button
-    //      - Enable Autoscroll and show indicator (when build is running)
-    // 3) User is somewhere in middle of Build Log;
-    //      - Show Top Arrow button
-    //      - Show Bottom Arrow button
-    //      - Disable Autoscroll and hide indicator (when build is running)
-    Build.prototype.initScrollMonitor = function() {
-      if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
-        // User is somewhere in middle of Build Log
-
-        this.$scrollTopBtn.show();
-
-        if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
-          this.$scrollBottomBtn.show();
-        } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
-          this.$scrollBottomBtn.show();
-        } else {
-          this.$scrollBottomBtn.hide();
-        }
-
-        // Hide Autoscroll Status Indicator
-        if (this.$scrollBottomBtn.is(':visible')) {
-          this.$autoScrollContainer.hide();
-          this.$autoScrollStatusText.removeClass('animate');
-        } else {
-          this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
-          this.$autoScrollStatusText.addClass('animate');
-        }
-      } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
-        // User is at Top of Build Log
+            return gl.utils.visitUrl(pageUrl);
+          }
+        };
+      })(this)
+    });
+  };
+
+  Build.prototype.checkAutoscroll = function() {
+    if (this.$autoScrollStatus.data("state") === "enabled") {
+      return $("html,body").scrollTop(this.$buildTrace.height());
+    }
 
-        this.$scrollTopBtn.hide();
+    // Handle a situation where user started new build
+    // but never scrolled a page
+    if (!this.$scrollTopBtn.is(':visible') &&
+        !this.$scrollBottomBtn.is(':visible') &&
+        !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+      this.$scrollBottomBtn.show();
+    }
+  };
+
+  Build.prototype.initScrollButtonAffix = function() {
+    // Hide everything initially
+    this.$scrollTopBtn.hide();
+    this.$scrollBottomBtn.hide();
+    this.$autoScrollContainer.hide();
+  };
+
+  // Page scroll listener to detect if user has scrolling page
+  // and handle following cases
+  // 1) User is at Top of Build Log;
+  //      - Hide Top Arrow button
+  //      - Show Bottom Arrow button
+  //      - Disable Autoscroll and hide indicator (when build is running)
+  // 2) User is at Bottom of Build Log;
+  //      - Show Top Arrow button
+  //      - Hide Bottom Arrow button
+  //      - Enable Autoscroll and show indicator (when build is running)
+  // 3) User is somewhere in middle of Build Log;
+  //      - Show Top Arrow button
+  //      - Show Bottom Arrow button
+  //      - Disable Autoscroll and hide indicator (when build is running)
+  Build.prototype.initScrollMonitor = function() {
+    if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+      // User is somewhere in middle of Build Log
+
+      this.$scrollTopBtn.show();
+
+      if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+        this.$scrollBottomBtn.show();
+      } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
         this.$scrollBottomBtn.show();
+      } else {
+        this.$scrollBottomBtn.hide();
+      }
 
+      // Hide Autoscroll Status Indicator
+      if (this.$scrollBottomBtn.is(':visible')) {
         this.$autoScrollContainer.hide();
         this.$autoScrollStatusText.removeClass('animate');
-      } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
-                 (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
-        // User is at Bottom of Build Log
-
-        this.$scrollTopBtn.show();
-        this.$scrollBottomBtn.hide();
-
-        // Show and Reposition Autoscroll Status Indicator
+      } else {
         this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
         this.$autoScrollStatusText.addClass('animate');
-      } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
-        // Build Log height is small
+      }
+    } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+      // User is at Top of Build Log
 
-        this.$scrollTopBtn.hide();
-        this.$scrollBottomBtn.hide();
+      this.$scrollTopBtn.hide();
+      this.$scrollBottomBtn.show();
 
-        // Hide Autoscroll Status Indicator
-        this.$autoScrollContainer.hide();
-        this.$autoScrollStatusText.removeClass('animate');
-      }
+      this.$autoScrollContainer.hide();
+      this.$autoScrollStatusText.removeClass('animate');
+    } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+               (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+      // User is at Bottom of Build Log
 
-      if (this.buildStatus === "running" || this.buildStatus === "pending") {
-        // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
-        this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
-      }
-    };
-
-    Build.prototype.shouldHideSidebarForViewport = function() {
-      var bootstrapBreakpoint;
-      bootstrapBreakpoint = this.bp.getBreakpointSize();
-      return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
-    };
-
-    Build.prototype.toggleSidebar = function(shouldHide) {
-      var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
-      this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
-        .toggleClass('sidebar-collapsed', shouldHide);
-      this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
-        .toggleClass('right-sidebar-collapsed', shouldHide);
-    };
-
-    Build.prototype.sidebarOnResize = function() {
-      this.toggleSidebar(this.shouldHideSidebarForViewport());
-    };
-
-    Build.prototype.sidebarOnClick = function() {
-      if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
-    };
-
-    Build.prototype.updateArtifactRemoveDate = function() {
-      var $date, date;
-      $date = $('.js-artifacts-remove');
-      if ($date.length) {
-        date = $date.text();
-        return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
-      }
-    };
-
-    Build.prototype.populateJobs = function(stage) {
-      $('.build-job').hide();
-      $('.build-job[data-stage="' + stage + '"]').show();
-    };
-
-    Build.prototype.updateStageDropdownText = function(stage) {
-      $('.stage-selection').text(stage);
-    };
-
-    Build.prototype.updateDropdown = function(e) {
-      e.preventDefault();
-      var stage = e.currentTarget.text;
-      this.updateStageDropdownText(stage);
-      this.populateJobs(stage);
-    };
-
-    Build.prototype.stepTrace = function(e) {
-      var $currentTarget;
-      e.preventDefault();
-      $currentTarget = $(e.currentTarget);
-      $.scrollTo($currentTarget.attr('href'), {
-        offset: 0
-      });
-    };
-
-    return Build;
-  })();
-}).call(window);
+      this.$scrollTopBtn.show();
+      this.$scrollBottomBtn.hide();
+
+      // Show and Reposition Autoscroll Status Indicator
+      this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+      this.$autoScrollStatusText.addClass('animate');
+    } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+      // Build Log height is small
+
+      this.$scrollTopBtn.hide();
+      this.$scrollBottomBtn.hide();
+
+      // Hide Autoscroll Status Indicator
+      this.$autoScrollContainer.hide();
+      this.$autoScrollStatusText.removeClass('animate');
+    }
+
+    if (this.buildStatus === "running" || this.buildStatus === "pending") {
+      // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+      this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+    }
+  };
+
+  Build.prototype.shouldHideSidebarForViewport = function() {
+    var bootstrapBreakpoint;
+    bootstrapBreakpoint = this.bp.getBreakpointSize();
+    return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+  };
+
+  Build.prototype.toggleSidebar = function(shouldHide) {
+    var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+    this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+      .toggleClass('sidebar-collapsed', shouldHide);
+    this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+      .toggleClass('right-sidebar-collapsed', shouldHide);
+  };
+
+  Build.prototype.sidebarOnResize = function() {
+    this.toggleSidebar(this.shouldHideSidebarForViewport());
+  };
+
+  Build.prototype.sidebarOnClick = function() {
+    if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
+  };
+
+  Build.prototype.updateArtifactRemoveDate = function() {
+    var $date, date;
+    $date = $('.js-artifacts-remove');
+    if ($date.length) {
+      date = $date.text();
+      return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+    }
+  };
+
+  Build.prototype.populateJobs = function(stage) {
+    $('.build-job').hide();
+    $('.build-job[data-stage="' + stage + '"]').show();
+  };
+
+  Build.prototype.updateStageDropdownText = function(stage) {
+    $('.stage-selection').text(stage);
+  };
+
+  Build.prototype.updateDropdown = function(e) {
+    e.preventDefault();
+    var stage = e.currentTarget.text;
+    this.updateStageDropdownText(stage);
+    this.populateJobs(stage);
+  };
+
+  Build.prototype.stepTrace = function(e) {
+    var $currentTarget;
+    e.preventDefault();
+    $currentTarget = $(e.currentTarget);
+    $.scrollTo($currentTarget.attr('href'), {
+      offset: 0
+    });
+  };
+
+  return Build;
+})();
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index cae9a0ffca46f25aa421b5be359c4f514f6702ac..bd479700fd32061ca95e028c600e48ac273b6586 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,26 +1,25 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
-(function() {
-  this.BuildArtifacts = (function() {
-    function BuildArtifacts() {
-      this.disablePropagation();
-      this.setupEntryClick();
-    }
 
-    BuildArtifacts.prototype.disablePropagation = function() {
-      $('.top-block').on('click', '.download', function(e) {
-        return e.stopPropagation();
-      });
-      return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
-        return e.stopImmediatePropagation();
-      });
-    };
+window.BuildArtifacts = (function() {
+  function BuildArtifacts() {
+    this.disablePropagation();
+    this.setupEntryClick();
+  }
 
-    BuildArtifacts.prototype.setupEntryClick = function() {
-      return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
-        return window.location = this.dataset.link;
-      });
-    };
+  BuildArtifacts.prototype.disablePropagation = function() {
+    $('.top-block').on('click', '.download', function(e) {
+      return e.stopPropagation();
+    });
+    return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+      return e.stopImmediatePropagation();
+    });
+  };
 
-    return BuildArtifacts;
-  })();
-}).call(window);
+  BuildArtifacts.prototype.setupEntryClick = function() {
+    return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
+      return window.location = this.dataset.link;
+    });
+  };
+
+  return BuildArtifacts;
+})();
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js
similarity index 100%
rename from app/assets/javascripts/build_variables.js.es6
rename to app/assets/javascripts/build_variables.js
diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd4a08a2f315006cbd3f439500c7f8b8e290c657
--- /dev/null
+++ b/app/assets/javascripts/ci_lint_editor.js
@@ -0,0 +1,17 @@
+
+window.gl = window.gl || {};
+
+class CILintEditor {
+  constructor() {
+    this.editor = window.ace.edit('ci-editor');
+    this.textarea = document.querySelector('#content');
+
+    this.editor.getSession().setMode('ace/mode/yaml');
+    this.editor.on('input', () => {
+      const content = this.editor.getSession().getValue();
+      this.textarea.value = content;
+    });
+  }
+}
+
+gl.CILintEditor = CILintEditor;
diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6
deleted file mode 100644
index 56ffaa765a8cb1d5c3bc42c54d93231e3e504fb9..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/ci_lint_editor.js.es6
+++ /dev/null
@@ -1,18 +0,0 @@
-(() => {
-  window.gl = window.gl || {};
-
-  class CILintEditor {
-    constructor() {
-      this.editor = window.ace.edit('ci-editor');
-      this.textarea = document.querySelector('#content');
-
-      this.editor.getSession().setMode('ace/mode/yaml');
-      this.editor.on('input', () => {
-        const content = this.editor.getSession().getValue();
-        this.textarea.value = content;
-      });
-    }
-  }
-
-  gl.CILintEditor = CILintEditor;
-})();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 566b322eb4906c947376c74d1a33c72045c63fcc..5f637524e30065742efd4c16006359b58b8c3202 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,14 +1,12 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife */
 /* global CommitFile */
 
-(function() {
-  this.Commit = (function() {
-    function Commit() {
-      $('.files .diff-file').each(function() {
-        return new CommitFile(this);
-      });
-    }
+window.Commit = (function() {
+  function Commit() {
+    $('.files .diff-file').each(function() {
+      return new CommitFile(this);
+    });
+  }
 
-    return Commit;
-  })();
-}).call(window);
+  return Commit;
+})();
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 49bb64a3472e2baabb5efcea6816be60b03a6c1d..17d14dc1e799cd7d8262a895225594d2cb305d74 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -52,6 +52,30 @@
       return this.views[viewMode].call(this);
     };
 
+    ImageFile.prototype.initDraggable = function($el, padding, callback) {
+      var dragging = false;
+      var $body = $('body');
+      var $offsetEl = $el.parent();
+
+      $el.off('mousedown').on('mousedown', function() {
+        dragging = true;
+        $body.css('user-select', 'none');
+      });
+
+      $body.off('mouseup').off('mousemove').on('mouseup', function() {
+        dragging = false;
+        $body.css('user-select', '');
+      })
+      .on('mousemove', function(e) {
+        var left;
+        if (!dragging) return;
+
+        left = e.pageX - ($offsetEl.offset().left + padding);
+
+        callback(e, left);
+      });
+    };
+
     prepareFrames = function(view) {
       var maxHeight, maxWidth;
       maxWidth = 0;
@@ -96,26 +120,30 @@
         maxHeight = 0;
         return $('.swipe.view', this.file).each((function(_this) {
           return function(index, view) {
-            var ref;
+            var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
             ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
-            $('.swipe-frame', view).css({
+            $swipeFrame = $('.swipe-frame', view);
+            $swipeWrap = $('.swipe-wrap', view);
+            $swipeBar = $('.swipe-bar', view);
+
+            $swipeFrame.css({
               width: maxWidth + 16,
               height: maxHeight + 28
             });
-            $('.swipe-wrap', view).css({
+            $swipeWrap.css({
               width: maxWidth + 1,
               height: maxHeight + 2
             });
-            return $('.swipe-bar', view).css({
+            $swipeBar.css({
               left: 0
-            }).draggable({
-              axis: 'x',
-              containment: 'parent',
-              drag: function(event) {
-                return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
-              },
-              stop: function(event) {
-                return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
+            });
+
+            wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+            _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+              if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
+                $swipeWrap.width((maxWidth + 1) - left);
+                $swipeBar.css('left', left);
               }
             });
           };
@@ -128,9 +156,14 @@
         dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
         return $('.onion-skin.view', this.file).each((function(_this) {
           return function(index, view) {
-            var ref;
+            var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
             ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
-            $('.onion-skin-frame', view).css({
+            $frame = $('.onion-skin-frame', view);
+            $frameAdded = $('.frame.added', view);
+            $track = $('.drag-track', view);
+            $dragger = $('.dragger', $track);
+
+            $frame.css({
               width: maxWidth + 16,
               height: maxHeight + 28
             });
@@ -138,16 +171,18 @@
               width: maxWidth + 1,
               height: maxHeight + 2
             });
-            return $('.dragger', view).css({
+            $dragger.css({
               left: dragTrackWidth
-            }).draggable({
-              axis: 'x',
-              containment: 'parent',
-              drag: function(event) {
-                return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
-              },
-              stop: function(event) {
-                return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
+            });
+
+            framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+            _this.initDraggable($dragger, framePadding, function(e, left) {
+              var opacity = left / dragTrackWidth;
+
+              if (opacity >= 0 && opacity <= 1) {
+                $dragger.css('left', left);
+                $frameAdded.css('opacity', opacity);
               }
             });
           };
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
similarity index 77%
rename from app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
rename to app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df8975eeceda684c48c992573fc2b3fe3d..a9f2d462c31092c612df50193c44c1a62f64becc 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,9 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+import CommitPipelinesTable from './pipelines_table';
 
 window.Vue = require('vue');
-require('./pipelines_table');
+window.Vue.use(require('vue-resource'));
+
 /**
  * Commits View > Pipelines Tab > Pipelines Table.
  * Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +22,7 @@ $(() => {
   }
 
   const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
-  gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+  gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
 
   if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
     gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
deleted file mode 100644
index 8ae98f9bf978fd95d197109f4c80e054227c2561..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
-  /**
-   * FIXME: The url provided to request the pipelines in the new merge request
-   * page already has `.json`.
-   * This should be fixed when the endpoint is improved.
-   *
-   * @param  {String} root
-   */
-  constructor(root) {
-    let endpoint;
-
-    if (root.indexOf('.json') === -1) {
-      endpoint = `${root}.json`;
-    } else {
-      endpoint = root;
-    }
-    this.pipelines = Vue.resource(endpoint);
-  }
-
-  /**
-   * Given the root param provided when the class is initialized, will
-   * make a GET request.
-   *
-   * @return {Promise}
-   */
-  all() {
-    return this.pipelines.get();
-  }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
new file mode 100644
index 0000000000000000000000000000000000000000..832c4b1bd2ad1e6b16201766743d557ed228bc4d
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -0,0 +1,110 @@
+/* eslint-disable no-new*/
+/* global Flash */
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
+
+/**
+ *
+ * Uses `pipelines-table-component` to render Pipelines table with an API call.
+ * Endpoint is provided in HTML and passed as `endpoint`.
+ * We need a store to store the received environemnts.
+ * We need a service to communicate with the server.
+ *
+ * Necessary SVG in the table are provided as props. This should be refactored
+ * as soon as we have Webpack and can load them directly into JS files.
+ */
+
+export default Vue.component('pipelines-table', {
+  components: {
+    'pipelines-table-component': PipelinesTableComponent,
+  },
+
+  /**
+   * Accesses the DOM to provide the needed data.
+   * Returns the necessary props to render `pipelines-table-component` component.
+   *
+   * @return {Object}
+   */
+  data() {
+    const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+    const store = new PipelineStore();
+
+    return {
+      endpoint: pipelinesTableData.endpoint,
+      store,
+      state: store.state,
+      isLoading: false,
+    };
+  },
+
+  /**
+   * When the component is about to be mounted, tell the service to fetch the data
+   *
+   * A request to fetch the pipelines will be made.
+   * In case of a successfull response we will store the data in the provided
+   * store, in case of a failed response we need to warn the user.
+   *
+   */
+  beforeMount() {
+    this.service = new PipelinesService(this.endpoint);
+
+    this.fetchPipelines();
+
+    eventHub.$on('refreshPipelines', this.fetchPipelines);
+  },
+
+  beforeUpdate() {
+    if (this.state.pipelines.length && this.$children) {
+      this.store.startTimeAgoLoops.call(this, Vue);
+    }
+  },
+
+  beforeDestroyed() {
+    eventHub.$off('refreshPipelines');
+  },
+
+  methods: {
+    fetchPipelines() {
+      this.isLoading = true;
+      return this.service.getPipelines()
+        .then(response => response.json())
+        .then((json) => {
+          // depending of the endpoint the response can either bring a `pipelines` key or not.
+          const pipelines = json.pipelines || json;
+          this.store.storePipelines(pipelines);
+          this.isLoading = false;
+        })
+        .catch(() => {
+          this.isLoading = false;
+          new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+        });
+    },
+  },
+
+  template: `
+    <div class="pipelines">
+      <div class="realtime-loading" v-if="isLoading">
+        <i class="fa fa-spinner fa-spin"></i>
+      </div>
+
+      <div class="blank-state blank-state-no-icon"
+        v-if="!isLoading && state.pipelines.length === 0">
+        <h2 class="blank-state-title js-blank-state-title">
+          No pipelines to show
+        </h2>
+      </div>
+
+      <div class="table-holder pipelines"
+        v-if="!isLoading && state.pipelines.length > 0">
+        <pipelines-table-component
+          :pipelines="state.pipelines"
+          :service="service" />
+      </div>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
deleted file mode 100644
index e7c6c063413893ac74bc132bd3c152abb60d9d0a..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
+++ /dev/null
@@ -1,110 +0,0 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
-
-/**
- *
- * Uses `pipelines-table-component` to render Pipelines table with an API call.
- * Endpoint is provided in HTML and passed as `endpoint`.
- * We need a store to store the received environemnts.
- * We need a service to communicate with the server.
- *
- * Necessary SVG in the table are provided as props. This should be refactored
- * as soon as we have Webpack and can load them directly into JS files.
- */
-
-(() => {
-  window.gl = window.gl || {};
-  gl.commits = gl.commits || {};
-  gl.commits.pipelines = gl.commits.pipelines || {};
-
-  gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
-
-    components: {
-      'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
-    },
-
-    /**
-     * Accesses the DOM to provide the needed data.
-     * Returns the necessary props to render `pipelines-table-component` component.
-     *
-     * @return {Object}
-     */
-    data() {
-      const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
-      const svgsData = document.querySelector('.pipeline-svgs').dataset;
-      const store = new PipelineStore();
-
-      // Transform svgs DOMStringMap to a plain Object.
-      const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
-
-      return {
-        endpoint: pipelinesTableData.endpoint,
-        svgs: svgsObject,
-        store,
-        state: store.state,
-        isLoading: false,
-      };
-    },
-
-    /**
-     * When the component is about to be mounted, tell the service to fetch the data
-     *
-     * A request to fetch the pipelines will be made.
-     * In case of a successfull response we will store the data in the provided
-     * store, in case of a failed response we need to warn the user.
-     *
-     */
-    beforeMount() {
-      const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
-
-      this.isLoading = true;
-      return pipelinesService.all()
-        .then(response => response.json())
-        .then((json) => {
-          this.store.storePipelines(json);
-          this.isLoading = false;
-        })
-        .catch(() => {
-          this.isLoading = false;
-          new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
-        });
-    },
-
-    beforeUpdate() {
-      if (this.state.pipelines.length && this.$children) {
-        PipelineStore.startTimeAgoLoops.call(this, Vue);
-      }
-    },
-
-    template: `
-      <div class="pipelines">
-        <div class="realtime-loading" v-if="isLoading">
-          <i class="fa fa-spinner fa-spin"></i>
-        </div>
-
-        <div class="blank-state blank-state-no-icon"
-          v-if="!isLoading && state.pipelines.length === 0">
-          <h2 class="blank-state-title js-blank-state-title">
-            No pipelines to show
-          </h2>
-        </div>
-
-        <div class="table-holder pipelines"
-          v-if="!isLoading && state.pipelines.length > 0">
-          <pipelines-table-component
-            :pipelines="state.pipelines"
-            :svgs="svgs">
-          </pipelines-table-component>
-        </div>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ccd895f3bf4e7cb8377096586228565ab122aef8..e3f9eaaf39cdacd1802756d78285d976e160f73b 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,68 +1,66 @@
 /* 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, max-len, prefer-arrow-callback */
 /* global Pager */
 
-(function() {
-  this.CommitsList = (function() {
-    var CommitsList = {};
+window.CommitsList = (function() {
+  var CommitsList = {};
 
-    CommitsList.timer = null;
+  CommitsList.timer = null;
 
-    CommitsList.init = function(limit) {
-      $("body").on("click", ".day-commits-table li.commit", function(e) {
-        if (e.target.nodeName !== "A") {
-          location.href = $(this).attr("url");
-          e.stopPropagation();
-          return false;
-        }
-      });
-      Pager.init(limit, false, false, function() {
-        gl.utils.localTimeAgo($('.js-timeago'));
-      });
-      this.content = $("#commits-list");
-      this.searchField = $("#commits-search");
-      this.lastSearch = this.searchField.val();
-      return this.initSearch();
-    };
+  CommitsList.init = function(limit) {
+    $("body").on("click", ".day-commits-table li.commit", function(e) {
+      if (e.target.nodeName !== "A") {
+        location.href = $(this).attr("url");
+        e.stopPropagation();
+        return false;
+      }
+    });
+    Pager.init(limit, false, false, function() {
+      gl.utils.localTimeAgo($('.js-timeago'));
+    });
+    this.content = $("#commits-list");
+    this.searchField = $("#commits-search");
+    this.lastSearch = this.searchField.val();
+    return this.initSearch();
+  };
 
-    CommitsList.initSearch = function() {
-      this.timer = null;
-      return this.searchField.keyup((function(_this) {
-        return function() {
-          clearTimeout(_this.timer);
-          return _this.timer = setTimeout(_this.filterResults, 500);
-        };
-      })(this));
-    };
+  CommitsList.initSearch = function() {
+    this.timer = null;
+    return this.searchField.keyup((function(_this) {
+      return function() {
+        clearTimeout(_this.timer);
+        return _this.timer = setTimeout(_this.filterResults, 500);
+      };
+    })(this));
+  };
 
-    CommitsList.filterResults = function() {
-      var commitsUrl, form, search;
-      form = $(".commits-search-form");
-      search = CommitsList.searchField.val();
-      if (search === CommitsList.lastSearch) return;
-      commitsUrl = form.attr("action") + '?' + form.serialize();
-      CommitsList.content.fadeTo('fast', 0.5);
-      return $.ajax({
-        type: "GET",
-        url: form.attr("action"),
-        data: form.serialize(),
-        complete: function() {
-          return CommitsList.content.fadeTo('fast', 1.0);
-        },
-        success: function(data) {
-          CommitsList.lastSearch = search;
-          CommitsList.content.html(data.html);
-          return history.replaceState({
-            page: commitsUrl
-          // Change url so if user reload a page - search results are saved
-          }, document.title, commitsUrl);
-        },
-        error: function() {
-          CommitsList.lastSearch = null;
-        },
-        dataType: "json"
-      });
-    };
+  CommitsList.filterResults = function() {
+    var commitsUrl, form, search;
+    form = $(".commits-search-form");
+    search = CommitsList.searchField.val();
+    if (search === CommitsList.lastSearch) return;
+    commitsUrl = form.attr("action") + '?' + form.serialize();
+    CommitsList.content.fadeTo('fast', 0.5);
+    return $.ajax({
+      type: "GET",
+      url: form.attr("action"),
+      data: form.serialize(),
+      complete: function() {
+        return CommitsList.content.fadeTo('fast', 1.0);
+      },
+      success: function(data) {
+        CommitsList.lastSearch = search;
+        CommitsList.content.html(data.html);
+        return history.replaceState({
+          page: commitsUrl
+        // Change url so if user reload a page - search results are saved
+        }, document.title, commitsUrl);
+      },
+      error: function() {
+        CommitsList.lastSearch = null;
+      },
+      dataType: "json"
+    });
+  };
 
-    return CommitsList;
-  })();
-}).call(window);
+  return CommitsList;
+})();
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
new file mode 100644
index 0000000000000000000000000000000000000000..36bfe457be90762dac1a81e0a21840b91d98d248
--- /dev/null
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -0,0 +1,16 @@
+import $ from 'jquery';
+
+// bootstrap jQuery plugins
+import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
+import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
+import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
+import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
+import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
+import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
+import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+
+// custom jQuery functions
+$.fn.extend({
+  disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); },
+  enable() { return $(this).removeAttr('disabled').removeClass('disabled'); },
+});
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7063f59d446abcdc52630c66aad3526905c3d678
--- /dev/null
+++ b/app/assets/javascripts/commons/index.js
@@ -0,0 +1,3 @@
+import './polyfills';
+import './jquery';
+import './bootstrap';
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
new file mode 100644
index 0000000000000000000000000000000000000000..b53f6284afcf8177980d358a0327bd7f9115bd90
--- /dev/null
+++ b/app/assets/javascripts/commons/jquery.js
@@ -0,0 +1,11 @@
+import 'jquery';
+
+// common jQuery plugins
+import 'jquery-ujs';
+import 'vendor/jquery.endless-scroll';
+import 'vendor/jquery.caret';
+import 'vendor/jquery.atwho';
+import 'vendor/jquery.scrollTo';
+import 'vendor/jquery.nicescroll';
+import 'vendor/jquery.waitforimages';
+import 'select2/select2';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
new file mode 100644
index 0000000000000000000000000000000000000000..fbd0db64ca7a8bc9e8471417f87ea3ac967d61d9
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -0,0 +1,10 @@
+// ECMAScript polyfills
+import 'core-js/fn/array/find';
+import 'core-js/fn/object/assign';
+import 'core-js/fn/promise';
+import 'core-js/fn/string/code-point-at';
+import 'core-js/fn/string/from-code-point';
+
+// Browser polyfills
+import './polyfills/custom_event';
+import './polyfills/element';
diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js
new file mode 100644
index 0000000000000000000000000000000000000000..aea61b82d03b3105b7970c97ca630000e07cfeb7
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/custom_event.js
@@ -0,0 +1,9 @@
+if (typeof window.CustomEvent !== 'function') {
+  window.CustomEvent = function CustomEvent(event, params) {
+    const evt = document.createEvent('CustomEvent');
+    const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
+    evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
+    return evt;
+  };
+  window.CustomEvent.prototype = Event;
+}
diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a1f73bf2ac432928907c8eee3e54760fe70cbe8
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/element.js
@@ -0,0 +1,20 @@
+Element.prototype.closest = Element.prototype.closest ||
+  function closest(selector, selectedElement = this) {
+    if (!selectedElement) return null;
+    return selectedElement.matches(selector) ?
+      selectedElement :
+      Element.prototype.closest(selector, selectedElement.parentElement);
+  };
+
+Element.prototype.matches = Element.prototype.matches ||
+  Element.prototype.matchesSelector ||
+  Element.prototype.mozMatchesSelector ||
+  Element.prototype.msMatchesSelector ||
+  Element.prototype.oMatchesSelector ||
+  Element.prototype.webkitMatchesSelector ||
+  function matches(selector) {
+    const elms = (this.document || this.ownerDocument).querySelectorAll(selector);
+    let i = elms.length - 1;
+    while (i >= 0 && elms.item(i) !== this) { i -= 1; }
+    return i > -1;
+  };
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 15df105d4ccb308038a79b3c8683c3879e75ae8e..9e5dbd64a7efa019ead39382ba8c8d51589ebd8d 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,91 +1,90 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-(function() {
-  this.Compare = (function() {
-    function Compare(opts) {
-      this.opts = opts;
-      this.source_loading = $(".js-source-loading");
-      this.target_loading = $(".js-target-loading");
-      $('.js-compare-dropdown').each((function(_this) {
-        return function(i, dropdown) {
-          var $dropdown;
-          $dropdown = $(dropdown);
-          return $dropdown.glDropdown({
-            selectable: true,
-            fieldName: $dropdown.data('field-name'),
-            filterable: true,
-            id: function(obj, $el) {
-              return $el.data('id');
-            },
-            toggleLabel: function(obj, $el) {
-              return $el.text().trim();
-            },
-            clicked: function(e, el) {
-              if ($dropdown.is('.js-target-branch')) {
-                return _this.getTargetHtml();
-              } else if ($dropdown.is('.js-source-branch')) {
-                return _this.getSourceHtml();
-              } else if ($dropdown.is('.js-target-project')) {
-                return _this.getTargetProject();
-              }
+
+window.Compare = (function() {
+  function Compare(opts) {
+    this.opts = opts;
+    this.source_loading = $(".js-source-loading");
+    this.target_loading = $(".js-target-loading");
+    $('.js-compare-dropdown').each((function(_this) {
+      return function(i, dropdown) {
+        var $dropdown;
+        $dropdown = $(dropdown);
+        return $dropdown.glDropdown({
+          selectable: true,
+          fieldName: $dropdown.data('field-name'),
+          filterable: true,
+          id: function(obj, $el) {
+            return $el.data('id');
+          },
+          toggleLabel: function(obj, $el) {
+            return $el.text().trim();
+          },
+          clicked: function(e, el) {
+            if ($dropdown.is('.js-target-branch')) {
+              return _this.getTargetHtml();
+            } else if ($dropdown.is('.js-source-branch')) {
+              return _this.getSourceHtml();
+            } else if ($dropdown.is('.js-target-project')) {
+              return _this.getTargetProject();
             }
-          });
-        };
-      })(this));
-      this.initialState();
-    }
+          }
+        });
+      };
+    })(this));
+    this.initialState();
+  }
 
-    Compare.prototype.initialState = function() {
-      this.getSourceHtml();
-      return this.getTargetHtml();
-    };
+  Compare.prototype.initialState = function() {
+    this.getSourceHtml();
+    return this.getTargetHtml();
+  };
 
-    Compare.prototype.getTargetProject = function() {
-      return $.ajax({
-        url: this.opts.targetProjectUrl,
-        data: {
-          target_project_id: $("input[name='merge_request[target_project_id]']").val()
-        },
-        beforeSend: function() {
-          return $('.mr_target_commit').empty();
-        },
-        success: function(html) {
-          return $('.js-target-branch-dropdown .dropdown-content').html(html);
-        }
-      });
-    };
+  Compare.prototype.getTargetProject = function() {
+    return $.ajax({
+      url: this.opts.targetProjectUrl,
+      data: {
+        target_project_id: $("input[name='merge_request[target_project_id]']").val()
+      },
+      beforeSend: function() {
+        return $('.mr_target_commit').empty();
+      },
+      success: function(html) {
+        return $('.js-target-branch-dropdown .dropdown-content').html(html);
+      }
+    });
+  };
 
-    Compare.prototype.getSourceHtml = function() {
-      return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
-        ref: $("input[name='merge_request[source_branch]']").val()
-      });
-    };
+  Compare.prototype.getSourceHtml = function() {
+    return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
+      ref: $("input[name='merge_request[source_branch]']").val()
+    });
+  };
 
-    Compare.prototype.getTargetHtml = function() {
-      return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
-        target_project_id: $("input[name='merge_request[target_project_id]']").val(),
-        ref: $("input[name='merge_request[target_branch]']").val()
-      });
-    };
+  Compare.prototype.getTargetHtml = function() {
+    return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
+      target_project_id: $("input[name='merge_request[target_project_id]']").val(),
+      ref: $("input[name='merge_request[target_branch]']").val()
+    });
+  };
 
-    Compare.prototype.sendAjax = function(url, loading, target, data) {
-      var $target;
-      $target = $(target);
-      return $.ajax({
-        url: url,
-        data: data,
-        beforeSend: function() {
-          loading.show();
-          return $target.empty();
-        },
-        success: function(html) {
-          loading.hide();
-          $target.html(html);
-          var className = '.' + $target[0].className.replace(' ', '.');
-          gl.utils.localTimeAgo($('.js-timeago', className));
-        }
-      });
-    };
+  Compare.prototype.sendAjax = function(url, loading, target, data) {
+    var $target;
+    $target = $(target);
+    return $.ajax({
+      url: url,
+      data: data,
+      beforeSend: function() {
+        loading.show();
+        return $target.empty();
+      },
+      success: function(html) {
+        loading.hide();
+        $target.html(html);
+        var className = '.' + $target[0].className.replace(' ', '.');
+        gl.utils.localTimeAgo($('.js-timeago', className));
+      }
+    });
+  };
 
-    return Compare;
-  })();
-}).call(window);
+  return Compare;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
new file mode 100644
index 0000000000000000000000000000000000000000..72c0d98d47c664b612fa7be423b4fd0c307beab0
--- /dev/null
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -0,0 +1,68 @@
+/* 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, wrap-iife, max-len */
+
+window.CompareAutocomplete = (function() {
+  function CompareAutocomplete() {
+    this.initDropdown();
+  }
+
+  CompareAutocomplete.prototype.initDropdown = function() {
+    return $('.js-compare-dropdown').each(function() {
+      var $dropdown, selected;
+      $dropdown = $(this);
+      selected = $dropdown.data('selected');
+      const $dropdownContainer = $dropdown.closest('.dropdown');
+      const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
+      const $filterInput = $('input[type="search"]', $dropdownContainer);
+      $dropdown.glDropdown({
+        data: function(term, callback) {
+          return $.ajax({
+            url: $dropdown.data('refs-url'),
+            data: {
+              ref: $dropdown.data('ref'),
+              search: term,
+            }
+          }).done(function(refs) {
+            return callback(refs);
+          });
+        },
+        selectable: true,
+        filterable: true,
+        filterRemote: true,
+        fieldName: $dropdown.data('field-name'),
+        filterInput: 'input[type="search"]',
+        renderRow: function(ref) {
+          var link;
+          if (ref.header != null) {
+            return $('<li />').addClass('dropdown-header').text(ref.header);
+          } else {
+            link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+            return $('<li />').append(link);
+          }
+        },
+        id: function(obj, $el) {
+          return $el.attr('data-ref');
+        },
+        toggleLabel: function(obj, $el) {
+          return $el.text().trim();
+        }
+      });
+      $filterInput.on('keyup', (e) => {
+        const keyCode = e.keyCode || e.which;
+        if (keyCode !== 13) return;
+        const text = $filterInput.val();
+        $fieldInput.val(text);
+        $('.dropdown-toggle-text', $dropdown).text(text);
+        $dropdownContainer.removeClass('open');
+      });
+
+      $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+        $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+        if ($dropdown.hasClass('has-tooltip')) {
+          $dropdown.tooltip('fixTitle');
+        }
+      });
+    });
+  };
+
+  return CompareAutocomplete;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
deleted file mode 100644
index 1eca973e06907d6faf3c4e9bb739c1befc0d090f..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ /dev/null
@@ -1,69 +0,0 @@
-/* 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, wrap-iife, max-len */
-
-(function() {
-  this.CompareAutocomplete = (function() {
-    function CompareAutocomplete() {
-      this.initDropdown();
-    }
-
-    CompareAutocomplete.prototype.initDropdown = function() {
-      return $('.js-compare-dropdown').each(function() {
-        var $dropdown, selected;
-        $dropdown = $(this);
-        selected = $dropdown.data('selected');
-        const $dropdownContainer = $dropdown.closest('.dropdown');
-        const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
-        const $filterInput = $('input[type="search"]', $dropdownContainer);
-        $dropdown.glDropdown({
-          data: function(term, callback) {
-            return $.ajax({
-              url: $dropdown.data('refs-url'),
-              data: {
-                ref: $dropdown.data('ref')
-              }
-            }).done(function(refs) {
-              return callback(refs);
-            });
-          },
-          selectable: true,
-          filterable: true,
-          filterByText: true,
-          fieldName: $dropdown.data('field-name'),
-          filterInput: 'input[type="search"]',
-          renderRow: function(ref) {
-            var link;
-            if (ref.header != null) {
-              return $('<li />').addClass('dropdown-header').text(ref.header);
-            } else {
-              link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
-              return $('<li />').append(link);
-            }
-          },
-          id: function(obj, $el) {
-            return $el.attr('data-ref');
-          },
-          toggleLabel: function(obj, $el) {
-            return $el.text().trim();
-          }
-        });
-        $filterInput.on('keyup', (e) => {
-          const keyCode = e.keyCode || e.which;
-          if (keyCode !== 13) return;
-          const text = $filterInput.val();
-          $fieldInput.val(text);
-          $('.dropdown-toggle-text', $dropdown).text(text);
-          $dropdownContainer.removeClass('open');
-        });
-
-        $dropdownContainer.on('click', '.dropdown-content a', (e) => {
-          $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
-          if ($dropdown.hasClass('has-tooltip')) {
-            $dropdown.tooltip('fixTitle');
-          }
-        });
-      });
-    };
-
-    return CompareAutocomplete;
-  })();
-}).call(window);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index a1c1b721228b353e7e5bf89197a8270946b0e6d9..b375b61202eea1b311fa3fd80440a76dcdc2f5cb 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,31 +1,30 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
-  this.ConfirmDangerModal = (function() {
-    function ConfirmDangerModal(form, text) {
-      var project_path, submit;
-      this.form = form;
-      $('.js-confirm-text').text(text || '');
-      $('.js-confirm-danger-input').val('');
-      $('#modal-confirm-danger').modal('show');
-      project_path = $('.js-confirm-danger-match').text();
-      submit = $('.js-confirm-danger-submit');
-      submit.disable();
-      $('.js-confirm-danger-input').off('input');
-      $('.js-confirm-danger-input').on('input', function() {
-        if (gl.utils.rstrip($(this).val()) === project_path) {
-          return submit.enable();
-        } else {
-          return submit.disable();
-        }
-      });
-      $('.js-confirm-danger-submit').off('click');
-      $('.js-confirm-danger-submit').on('click', (function(_this) {
-        return function() {
-          return _this.form.submit();
-        };
-      })(this));
-    }
 
-    return ConfirmDangerModal;
-  })();
-}).call(window);
+window.ConfirmDangerModal = (function() {
+  function ConfirmDangerModal(form, text) {
+    var project_path, submit;
+    this.form = form;
+    $('.js-confirm-text').text(text || '');
+    $('.js-confirm-danger-input').val('');
+    $('#modal-confirm-danger').modal('show');
+    project_path = $('.js-confirm-danger-match').text();
+    submit = $('.js-confirm-danger-submit');
+    submit.disable();
+    $('.js-confirm-danger-input').off('input');
+    $('.js-confirm-danger-input').on('input', function() {
+      if (gl.utils.rstrip($(this).val()) === project_path) {
+        return submit.enable();
+      } else {
+        return submit.disable();
+      }
+    });
+    $('.js-confirm-danger-submit').off('click');
+    $('.js-confirm-danger-submit').on('click', (function(_this) {
+      return function() {
+        return _this.form.submit();
+      };
+    })(this));
+  }
+
+  return ConfirmDangerModal;
+})();
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
new file mode 100644
index 0000000000000000000000000000000000000000..570799c030e47421bcfdd6ebbf8c92df25b8dda2
--- /dev/null
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -0,0 +1,402 @@
+/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+
+require('./lib/utils/common_utils');
+
+const gfmRules = {
+  // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
+  // GitLab Flavored Markdown (GFM) to HTML.
+  // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
+  // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+  // from GFM should have a handler here, in reverse order.
+  // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+  InlineDiffFilter: {
+    'span.idiff.addition'(el, text) {
+      return `{+${text}+}`;
+    },
+    'span.idiff.deletion'(el, text) {
+      return `{-${text}-}`;
+    },
+  },
+  TaskListFilter: {
+    'input[type=checkbox].task-list-item-checkbox'(el, text) {
+      return `[${el.checked ? 'x' : ' '}]`;
+    },
+  },
+  ReferenceFilter: {
+    '.tooltip'(el, text) {
+      return '';
+    },
+    'a.gfm:not([data-link=true])'(el, text) {
+      return el.dataset.original || text;
+    },
+  },
+  AutolinkFilter: {
+    'a'(el, text) {
+      // Fallback on the regular MarkdownFilter's `a` handler.
+      if (text !== el.getAttribute('href')) return false;
+
+      return text;
+    },
+  },
+  TableOfContentsFilter: {
+    'ul.section-nav'(el, text) {
+      return '[[_TOC_]]';
+    },
+  },
+  EmojiFilter: {
+    'img.emoji'(el, text) {
+      return el.getAttribute('alt');
+    },
+    'gl-emoji'(el, text) {
+      return `:${el.getAttribute('data-name')}:`;
+    },
+  },
+  ImageLinkFilter: {
+    'a.no-attachment-icon'(el, text) {
+      return text;
+    },
+  },
+  VideoLinkFilter: {
+    '.video-container'(el, text) {
+      const videoEl = el.querySelector('video');
+      if (!videoEl) return false;
+
+      return CopyAsGFM.nodeToGFM(videoEl);
+    },
+    'video'(el, text) {
+      return `![${el.dataset.title}](${el.getAttribute('src')})`;
+    },
+  },
+  MathFilter: {
+    'pre.code.math[data-math-style=display]'(el, text) {
+      return `\`\`\`math\n${text.trim()}\n\`\`\``;
+    },
+    'code.code.math[data-math-style=inline]'(el, text) {
+      return `$\`${text}\`$`;
+    },
+    'span.katex-display span.katex-mathml'(el, text) {
+      const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+      if (!mathAnnotation) return false;
+
+      return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
+    },
+    'span.katex-mathml'(el, text) {
+      const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+      if (!mathAnnotation) return false;
+
+      return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
+    },
+    'span.katex-html'(el, text) {
+      // We don't want to include the content of this element in the copied text.
+      return '';
+    },
+    'annotation[encoding="application/x-tex"]'(el, text) {
+      return text.trim();
+    },
+  },
+  SanitizationFilter: {
+    'a[name]:not([href]):empty'(el, text) {
+      return el.outerHTML;
+    },
+    'dl'(el, text) {
+      let lines = text.trim().split('\n');
+      // Add two spaces to the front of subsequent list items lines,
+      // or leave the line entirely blank.
+      lines = lines.map((l) => {
+        const line = l.trim();
+        if (line.length === 0) return '';
+
+        return `  ${line}`;
+      });
+
+      return `<dl>\n${lines.join('\n')}\n</dl>`;
+    },
+    'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+      const tag = el.nodeName.toLowerCase();
+      return `<${tag}>${text}</${tag}>`;
+    },
+  },
+  SyntaxHighlightFilter: {
+    'pre.code.highlight'(el, t) {
+      const text = t.trimRight();
+
+      let lang = el.getAttribute('lang');
+      if (!lang || lang === 'plaintext') {
+        lang = '';
+      }
+
+      // Prefixes lines with 4 spaces if the code contains triple backticks
+      if (lang === '' && text.match(/^```/gm)) {
+        return text.split('\n').map((l) => {
+          const line = l.trim();
+          if (line.length === 0) return '';
+
+          return `    ${line}`;
+        }).join('\n');
+      }
+
+      return `\`\`\`${lang}\n${text}\n\`\`\``;
+    },
+    'pre > code'(el, text) {
+       // Don't wrap code blocks in ``
+      return text;
+    },
+  },
+  MarkdownFilter: {
+    'br'(el, text) {
+      // Two spaces at the end of a line are turned into a BR
+      return '  ';
+    },
+    'code'(el, text) {
+      let backtickCount = 1;
+      const backtickMatch = text.match(/`+/);
+      if (backtickMatch) {
+        backtickCount = backtickMatch[0].length + 1;
+      }
+
+      const backticks = Array(backtickCount + 1).join('`');
+      const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
+
+      return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
+    },
+    'blockquote'(el, text) {
+      return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
+    },
+    'img'(el, text) {
+      return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+    },
+    'a.anchor'(el, text) {
+      // Don't render a Markdown link for the anchor link inside a heading
+      return text;
+    },
+    'a'(el, text) {
+      return `[${text}](${el.getAttribute('href')})`;
+    },
+    'li'(el, text) {
+      const lines = text.trim().split('\n');
+      const firstLine = `- ${lines.shift()}`;
+      // Add four spaces to the front of subsequent list items lines,
+      // or leave the line entirely blank.
+      const nextLines = lines.map((s) => {
+        if (s.trim().length === 0) return '';
+
+        return `    ${s}`;
+      });
+
+      return `${firstLine}\n${nextLines.join('\n')}`;
+    },
+    'ul'(el, text) {
+      return text;
+    },
+    'ol'(el, text) {
+      // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
+      return text.replace(/^- /mg, '1. ');
+    },
+    'h1'(el, text) {
+      return `# ${text.trim()}`;
+    },
+    'h2'(el, text) {
+      return `## ${text.trim()}`;
+    },
+    'h3'(el, text) {
+      return `### ${text.trim()}`;
+    },
+    'h4'(el, text) {
+      return `#### ${text.trim()}`;
+    },
+    'h5'(el, text) {
+      return `##### ${text.trim()}`;
+    },
+    'h6'(el, text) {
+      return `###### ${text.trim()}`;
+    },
+    'strong'(el, text) {
+      return `**${text}**`;
+    },
+    'em'(el, text) {
+      return `_${text}_`;
+    },
+    'del'(el, text) {
+      return `~~${text}~~`;
+    },
+    'sup'(el, text) {
+      return `^${text}`;
+    },
+    'hr'(el, text) {
+      return '-----';
+    },
+    'table'(el, text) {
+      const theadEl = el.querySelector('thead');
+      const tbodyEl = el.querySelector('tbody');
+      if (!theadEl || !tbodyEl) return false;
+
+      const theadText = CopyAsGFM.nodeToGFM(theadEl);
+      const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
+
+      return theadText + tbodyText;
+    },
+    'thead'(el, text) {
+      const cells = _.map(el.querySelectorAll('th'), (cell) => {
+        let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
+
+        let before = '';
+        let after = '';
+        switch (cell.style.textAlign) {
+          case 'center':
+            before = ':';
+            after = ':';
+            chars -= 2;
+            break;
+          case 'right':
+            after = ':';
+            chars -= 1;
+            break;
+          default:
+            break;
+        }
+
+        chars = Math.max(chars, 3);
+
+        const middle = Array(chars + 1).join('-');
+
+        return before + middle + after;
+      });
+
+      return `${text}|${cells.join('|')}|`;
+    },
+    'tr'(el, text) {
+      const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
+      return `| ${cells.join(' | ')} |`;
+    },
+  },
+};
+
+class CopyAsGFM {
+  constructor() {
+    $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
+    $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
+    $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this));
+  }
+
+  copyAsGFM(e, transformer) {
+    const clipboardData = e.originalEvent.clipboardData;
+    if (!clipboardData) return;
+
+    const documentFragment = window.gl.utils.getSelectedFragment();
+    if (!documentFragment) return;
+
+    const el = transformer(documentFragment.cloneNode(true));
+    if (!el) return;
+
+    e.preventDefault();
+    e.stopPropagation();
+
+    clipboardData.setData('text/plain', el.textContent);
+    clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
+  }
+
+  pasteGFM(e) {
+    const clipboardData = e.originalEvent.clipboardData;
+    if (!clipboardData) return;
+
+    const gfm = clipboardData.getData('text/x-gfm');
+    if (!gfm) return;
+
+    e.preventDefault();
+
+    window.gl.utils.insertText(e.target, gfm);
+  }
+
+  static transformGFMSelection(documentFragment) {
+    // If the documentFragment contains more than just Markdown, don't copy as GFM.
+    if (documentFragment.querySelector('.md, .wiki')) return null;
+
+    return documentFragment;
+  }
+
+  static transformCodeSelection(documentFragment) {
+    const lineEls = documentFragment.querySelectorAll('.line');
+
+    let codeEl;
+    if (lineEls.length > 1) {
+      codeEl = document.createElement('pre');
+      codeEl.className = 'code highlight';
+
+      const lang = lineEls[0].getAttribute('lang');
+      if (lang) {
+        codeEl.setAttribute('lang', lang);
+      }
+    } else {
+      codeEl = document.createElement('code');
+    }
+
+    if (lineEls.length > 0) {
+      for (let i = 0; i < lineEls.length; i += 1) {
+        const lineEl = lineEls[i];
+        codeEl.appendChild(lineEl);
+        codeEl.appendChild(document.createTextNode('\n'));
+      }
+    } else {
+      codeEl.appendChild(documentFragment);
+    }
+
+    return codeEl;
+  }
+
+  static nodeToGFM(node) {
+    if (node.nodeType === Node.COMMENT_NODE) {
+      return '';
+    }
+
+    if (node.nodeType === Node.TEXT_NODE) {
+      return node.textContent;
+    }
+
+    const text = this.innerGFM(node);
+
+    if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+      return text;
+    }
+
+    for (const filter in gfmRules) {
+      const rules = gfmRules[filter];
+
+      for (const selector in rules) {
+        const func = rules[selector];
+
+        if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
+
+        const result = func(node, text);
+        if (result === false) continue;
+
+        return result;
+      }
+    }
+
+    return text;
+  }
+
+  static innerGFM(parentNode) {
+    const nodes = parentNode.childNodes;
+
+    const clonedParentNode = parentNode.cloneNode(true);
+    const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+
+    for (let i = 0; i < nodes.length; i += 1) {
+      const node = nodes[i];
+      const clonedNode = clonedNodes[i];
+
+      const text = this.nodeToGFM(node);
+
+      // `clonedNode.replaceWith(text)` is not yet widely supported
+      clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
+    }
+
+    return clonedParentNode.innerText || clonedParentNode.textContent;
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.CopyAsGFM = CopyAsGFM;
+
+new CopyAsGFM();
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
deleted file mode 100644
index 4bd537a6f28ed51f12c92a5444345927a88c4a6c..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/copy_as_gfm.js.es6
+++ /dev/null
@@ -1,358 +0,0 @@
-/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
-/* jshint esversion: 6 */
-
-require('./lib/utils/common_utils');
-
-(() => {
-  const gfmRules = {
-    // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
-    // GitLab Flavored Markdown (GFM) to HTML.
-    // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
-    // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
-    // from GFM should have a handler here, in reverse order.
-    // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
-    InlineDiffFilter: {
-      'span.idiff.addition'(el, text) {
-        return `{+${text}+}`;
-      },
-      'span.idiff.deletion'(el, text) {
-        return `{-${text}-}`;
-      },
-    },
-    TaskListFilter: {
-      'input[type=checkbox].task-list-item-checkbox'(el, text) {
-        return `[${el.checked ? 'x' : ' '}]`;
-      },
-    },
-    ReferenceFilter: {
-      'a.gfm:not([data-link=true])'(el, text) {
-        return el.dataset.original || text;
-      },
-    },
-    AutolinkFilter: {
-      'a'(el, text) {
-        // Fallback on the regular MarkdownFilter's `a` handler.
-        if (text !== el.getAttribute('href')) return false;
-
-        return text;
-      },
-    },
-    TableOfContentsFilter: {
-      'ul.section-nav'(el, text) {
-        return '[[_TOC_]]';
-      },
-    },
-    EmojiFilter: {
-      'img.emoji'(el, text) {
-        return el.getAttribute('alt');
-      },
-    },
-    ImageLinkFilter: {
-      'a.no-attachment-icon'(el, text) {
-        return text;
-      },
-    },
-    VideoLinkFilter: {
-      '.video-container'(el, text) {
-        const videoEl = el.querySelector('video');
-        if (!videoEl) return false;
-
-        return CopyAsGFM.nodeToGFM(videoEl);
-      },
-      'video'(el, text) {
-        return `![${el.dataset.title}](${el.getAttribute('src')})`;
-      },
-    },
-    MathFilter: {
-      'pre.code.math[data-math-style=display]'(el, text) {
-        return `\`\`\`math\n${text.trim()}\n\`\`\``;
-      },
-      'code.code.math[data-math-style=inline]'(el, text) {
-        return `$\`${text}\`$`;
-      },
-      'span.katex-display span.katex-mathml'(el, text) {
-        const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
-        if (!mathAnnotation) return false;
-
-        return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
-      },
-      'span.katex-mathml'(el, text) {
-        const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
-        if (!mathAnnotation) return false;
-
-        return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
-      },
-      'span.katex-html'(el, text) {
-        // We don't want to include the content of this element in the copied text.
-        return '';
-      },
-      'annotation[encoding="application/x-tex"]'(el, text) {
-        return text.trim();
-      },
-    },
-    SanitizationFilter: {
-      'a[name]:not([href]):empty'(el, text) {
-        return el.outerHTML;
-      },
-      'dl'(el, text) {
-        let lines = text.trim().split('\n');
-        // Add two spaces to the front of subsequent list items lines,
-        // or leave the line entirely blank.
-        lines = lines.map((l) => {
-          const line = l.trim();
-          if (line.length === 0) return '';
-
-          return `  ${line}`;
-        });
-
-        return `<dl>\n${lines.join('\n')}\n</dl>`;
-      },
-      'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
-        const tag = el.nodeName.toLowerCase();
-        return `<${tag}>${text}</${tag}>`;
-      },
-    },
-    SyntaxHighlightFilter: {
-      'pre.code.highlight'(el, t) {
-        const text = t.trim();
-
-        let lang = el.getAttribute('lang');
-        if (lang === 'plaintext') {
-          lang = '';
-        }
-
-        // Prefixes lines with 4 spaces if the code contains triple backticks
-        if (lang === '' && text.match(/^```/gm)) {
-          return text.split('\n').map((l) => {
-            const line = l.trim();
-            if (line.length === 0) return '';
-
-            return `    ${line}`;
-          }).join('\n');
-        }
-
-        return `\`\`\`${lang}\n${text}\n\`\`\``;
-      },
-      'pre > code'(el, text) {
-         // Don't wrap code blocks in ``
-        return text;
-      },
-    },
-    MarkdownFilter: {
-      'br'(el, text) {
-        // Two spaces at the end of a line are turned into a BR
-        return '  ';
-      },
-      'code'(el, text) {
-        let backtickCount = 1;
-        const backtickMatch = text.match(/`+/);
-        if (backtickMatch) {
-          backtickCount = backtickMatch[0].length + 1;
-        }
-
-        const backticks = Array(backtickCount + 1).join('`');
-        const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
-
-        return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
-      },
-      'blockquote'(el, text) {
-        return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
-      },
-      'img'(el, text) {
-        return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
-      },
-      'a.anchor'(el, text) {
-        // Don't render a Markdown link for the anchor link inside a heading
-        return text;
-      },
-      'a'(el, text) {
-        return `[${text}](${el.getAttribute('href')})`;
-      },
-      'li'(el, text) {
-        const lines = text.trim().split('\n');
-        const firstLine = `- ${lines.shift()}`;
-        // Add four spaces to the front of subsequent list items lines,
-        // or leave the line entirely blank.
-        const nextLines = lines.map((s) => {
-          if (s.trim().length === 0) return '';
-
-          return `    ${s}`;
-        });
-
-        return `${firstLine}\n${nextLines.join('\n')}`;
-      },
-      'ul'(el, text) {
-        return text;
-      },
-      'ol'(el, text) {
-        // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
-        return text.replace(/^- /mg, '1. ');
-      },
-      'h1'(el, text) {
-        return `# ${text.trim()}`;
-      },
-      'h2'(el, text) {
-        return `## ${text.trim()}`;
-      },
-      'h3'(el, text) {
-        return `### ${text.trim()}`;
-      },
-      'h4'(el, text) {
-        return `#### ${text.trim()}`;
-      },
-      'h5'(el, text) {
-        return `##### ${text.trim()}`;
-      },
-      'h6'(el, text) {
-        return `###### ${text.trim()}`;
-      },
-      'strong'(el, text) {
-        return `**${text}**`;
-      },
-      'em'(el, text) {
-        return `_${text}_`;
-      },
-      'del'(el, text) {
-        return `~~${text}~~`;
-      },
-      'sup'(el, text) {
-        return `^${text}`;
-      },
-      'hr'(el, text) {
-        return '-----';
-      },
-      'table'(el, text) {
-        const theadEl = el.querySelector('thead');
-        const tbodyEl = el.querySelector('tbody');
-        if (!theadEl || !tbodyEl) return false;
-
-        const theadText = CopyAsGFM.nodeToGFM(theadEl);
-        const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
-
-        return theadText + tbodyText;
-      },
-      'thead'(el, text) {
-        const cells = _.map(el.querySelectorAll('th'), (cell) => {
-          let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
-
-          let before = '';
-          let after = '';
-          switch (cell.style.textAlign) {
-            case 'center':
-              before = ':';
-              after = ':';
-              chars -= 2;
-              break;
-            case 'right':
-              after = ':';
-              chars -= 1;
-              break;
-            default:
-              break;
-          }
-
-          chars = Math.max(chars, 3);
-
-          const middle = Array(chars + 1).join('-');
-
-          return before + middle + after;
-        });
-
-        return `${text}|${cells.join('|')}|`;
-      },
-      'tr'(el, text) {
-        const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
-        return `| ${cells.join(' | ')} |`;
-      },
-    },
-  };
-
-  class CopyAsGFM {
-    constructor() {
-      $(document).on('copy', '.md, .wiki', this.handleCopy);
-      $(document).on('paste', '.js-gfm-input', this.handlePaste);
-    }
-
-    handleCopy(e) {
-      const clipboardData = e.originalEvent.clipboardData;
-      if (!clipboardData) return;
-
-      const documentFragment = window.gl.utils.getSelectedFragment();
-      if (!documentFragment) return;
-
-      // If the documentFragment contains more than just Markdown, don't copy as GFM.
-      if (documentFragment.querySelector('.md, .wiki')) return;
-
-      e.preventDefault();
-      clipboardData.setData('text/plain', documentFragment.textContent);
-
-      const gfm = CopyAsGFM.nodeToGFM(documentFragment);
-      clipboardData.setData('text/x-gfm', gfm);
-    }
-
-    handlePaste(e) {
-      const clipboardData = e.originalEvent.clipboardData;
-      if (!clipboardData) return;
-
-      const gfm = clipboardData.getData('text/x-gfm');
-      if (!gfm) return;
-
-      e.preventDefault();
-
-      window.gl.utils.insertText(e.target, gfm);
-    }
-
-    static nodeToGFM(node) {
-      if (node.nodeType === Node.TEXT_NODE) {
-        return node.textContent;
-      }
-
-      const text = this.innerGFM(node);
-
-      if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
-        return text;
-      }
-
-      for (const filter in gfmRules) {
-        const rules = gfmRules[filter];
-
-        for (const selector in rules) {
-          const func = rules[selector];
-
-          if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
-
-          const result = func(node, text);
-          if (result === false) continue;
-
-          return result;
-        }
-      }
-
-      return text;
-    }
-
-    static innerGFM(parentNode) {
-      const nodes = parentNode.childNodes;
-
-      const clonedParentNode = parentNode.cloneNode(true);
-      const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
-
-      for (let i = 0; i < nodes.length; i += 1) {
-        const node = nodes[i];
-        const clonedNode = clonedNodes[i];
-
-        const text = this.nodeToGFM(node);
-
-        // `clonedNode.replaceWith(text)` is not yet widely supported
-        clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
-      }
-
-      return clonedParentNode.innerText || clonedParentNode.textContent;
-    }
-  }
-
-  window.gl = window.gl || {};
-  window.gl.CopyAsGFM = CopyAsGFM;
-
-  new CopyAsGFM();
-})();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 615f485e18acbab12543672bd92e4f31368f5d17..6dbec50b89079376b06108001a9f094a2b9f4990 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,49 +1,46 @@
 /* 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, max-len */
-/* global Clipboard */
-
-window.Clipboard = require('vendor/clipboard');
-
-(function() {
-  var genericError, genericSuccess, showTooltip;
-
-  genericSuccess = function(e) {
-    showTooltip(e.trigger, 'Copied');
-    // Clear the selection and blur the trigger so it loses its border
-    e.clearSelection();
-    return $(e.trigger).blur();
-  };
-
-  // Safari doesn't support `execCommand`, so instead we inform the user to
-  // copy manually.
-  //
-  // See http://clipboardjs.com/#browser-support
-  genericError = function(e) {
-    var key;
-    if (/Mac/i.test(navigator.userAgent)) {
-      key = '&#8984;'; // Command
-    } else {
-      key = 'Ctrl';
-    }
-    return showTooltip(e.trigger, "Press " + key + "-C to copy");
-  };
-
-  showTooltip = function(target, title) {
-    var $target = $(target);
-    var originalTitle = $target.data('original-title');
-
-    $target
-      .attr('title', 'Copied')
-      .tooltip('fixTitle')
-      .tooltip('show')
-      .attr('title', originalTitle)
-      .tooltip('fixTitle');
-  };
-
-  $(function() {
-    var clipboard;
-
-    clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
-    clipboard.on('success', genericSuccess);
-    return clipboard.on('error', genericError);
-  });
-}).call(window);
+
+import Clipboard from 'vendor/clipboard';
+
+var genericError, genericSuccess, showTooltip;
+
+genericSuccess = function(e) {
+  showTooltip(e.trigger, 'Copied');
+  // Clear the selection and blur the trigger so it loses its border
+  e.clearSelection();
+  return $(e.trigger).blur();
+};
+
+// Safari doesn't support `execCommand`, so instead we inform the user to
+// copy manually.
+//
+// See http://clipboardjs.com/#browser-support
+genericError = function(e) {
+  var key;
+  if (/Mac/i.test(navigator.userAgent)) {
+    key = '&#8984;'; // Command
+  } else {
+    key = 'Ctrl';
+  }
+  return showTooltip(e.trigger, "Press " + key + "-C to copy");
+};
+
+showTooltip = function(target, title) {
+  var $target = $(target);
+  var originalTitle = $target.data('original-title');
+
+  $target
+    .attr('title', 'Copied')
+    .tooltip('fixTitle')
+    .tooltip('show')
+    .attr('title', originalTitle)
+    .tooltip('fixTitle');
+};
+
+$(function() {
+  var clipboard;
+
+  clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+  clipboard.on('success', genericSuccess);
+  return clipboard.on('error', genericError);
+});
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
new file mode 100644
index 0000000000000000000000000000000000000000..121d64db7895d24463029ec8449b1d3c867faee4
--- /dev/null
+++ b/app/assets/javascripts/create_label.js
@@ -0,0 +1,127 @@
+/* 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 */
+
+class CreateLabelDropdown {
+  constructor ($el, namespacePath, projectPath) {
+    this.$el = $el;
+    this.namespacePath = namespacePath;
+    this.projectPath = projectPath;
+    this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+    this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+    this.$newLabelField = $('#new_label_name', this.$el);
+    this.$newColorField = $('#new_label_color', this.$el);
+    this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+    this.$newLabelError = $('.js-label-error', this.$el);
+    this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+    this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+    this.$newLabelError.hide();
+    this.$newLabelCreateButton.disable();
+
+    this.cleanBinding();
+    this.addBinding();
+  }
+
+  cleanBinding () {
+    this.$colorSuggestions.off('click');
+    this.$newLabelField.off('keyup change');
+    this.$newColorField.off('keyup change');
+    this.$dropdownBack.off('click');
+    this.$cancelButton.off('click');
+    this.$newLabelCreateButton.off('click');
+  }
+
+  addBinding () {
+    const self = this;
+
+    this.$colorSuggestions.on('click', function (e) {
+      const $this = $(this);
+      self.addColorValue(e, $this);
+    });
+
+    this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+    this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+
+    this.$dropdownBack.on('click', this.resetForm.bind(this));
+
+    this.$cancelButton.on('click', function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      self.resetForm();
+      self.$dropdownBack.trigger('click');
+    });
+
+    this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+  }
+
+  addColorValue (e, $this) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.$newColorField.val($this.data('color')).trigger('change');
+    this.$colorPreview
+      .css('background-color', $this.data('color'))
+      .parent()
+      .addClass('is-active');
+  }
+
+  enableLabelCreateButton () {
+    if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+      this.$newLabelError.hide();
+      this.$newLabelCreateButton.enable();
+    } else {
+      this.$newLabelCreateButton.disable();
+    }
+  }
+
+  resetForm () {
+    this.$newLabelField
+      .val('')
+      .trigger('change');
+
+    this.$newColorField
+      .val('')
+      .trigger('change');
+
+    this.$colorPreview
+      .css('background-color', '')
+      .parent()
+      .removeClass('is-active');
+  }
+
+  saveLabel (e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    Api.newLabel(this.namespacePath, this.projectPath, {
+      title: this.$newLabelField.val(),
+      color: this.$newColorField.val()
+    }, (label) => {
+      this.$newLabelCreateButton.enable();
+
+      if (label.message) {
+        let errors;
+
+        if (typeof label.message === 'string') {
+          errors = label.message;
+        } else {
+          errors = Object.keys(label.message).map(key =>
+            `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
+          ).join("<br/>");
+        }
+
+        this.$newLabelError
+          .html(errors)
+          .show();
+      } else {
+        this.$dropdownBack.trigger('click');
+
+        $(document).trigger('created.label', label);
+      }
+    });
+  }
+}
+
+window.gl = window.gl || {};
+gl.CreateLabelDropdown = CreateLabelDropdown;
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
deleted file mode 100644
index 85384d9812617e213aa87d08fb2c879643af196d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/create_label.js.es6
+++ /dev/null
@@ -1,132 +0,0 @@
-/* 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) {
-      this.$el = $el;
-      this.namespacePath = namespacePath;
-      this.projectPath = projectPath;
-      this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
-      this.$cancelButton = $('.js-cancel-label-btn', this.$el);
-      this.$newLabelField = $('#new_label_name', this.$el);
-      this.$newColorField = $('#new_label_color', this.$el);
-      this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
-      this.$newLabelError = $('.js-label-error', this.$el);
-      this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
-      this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
-
-      this.$newLabelError.hide();
-      this.$newLabelCreateButton.disable();
-
-      this.cleanBinding();
-      this.addBinding();
-    }
-
-    cleanBinding () {
-      this.$colorSuggestions.off('click');
-      this.$newLabelField.off('keyup change');
-      this.$newColorField.off('keyup change');
-      this.$dropdownBack.off('click');
-      this.$cancelButton.off('click');
-      this.$newLabelCreateButton.off('click');
-    }
-
-    addBinding () {
-      const self = this;
-
-      this.$colorSuggestions.on('click', function (e) {
-        const $this = $(this);
-        self.addColorValue(e, $this);
-      });
-
-      this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
-      this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
-
-      this.$dropdownBack.on('click', this.resetForm.bind(this));
-
-      this.$cancelButton.on('click', function(e) {
-        e.preventDefault();
-        e.stopPropagation();
-
-        self.resetForm();
-        self.$dropdownBack.trigger('click');
-      });
-
-      this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
-    }
-
-    addColorValue (e, $this) {
-      e.preventDefault();
-      e.stopPropagation();
-
-      this.$newColorField.val($this.data('color')).trigger('change');
-      this.$colorPreview
-        .css('background-color', $this.data('color'))
-        .parent()
-        .addClass('is-active');
-    }
-
-    enableLabelCreateButton () {
-      if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
-        this.$newLabelError.hide();
-        this.$newLabelCreateButton.enable();
-      } else {
-        this.$newLabelCreateButton.disable();
-      }
-    }
-
-    resetForm () {
-      this.$newLabelField
-        .val('')
-        .trigger('change');
-
-      this.$newColorField
-        .val('')
-        .trigger('change');
-
-      this.$colorPreview
-        .css('background-color', '')
-        .parent()
-        .removeClass('is-active');
-    }
-
-    saveLabel (e) {
-      e.preventDefault();
-      e.stopPropagation();
-
-      Api.newLabel(this.namespacePath, this.projectPath, {
-        title: this.$newLabelField.val(),
-        color: this.$newColorField.val()
-      }, (label) => {
-        this.$newLabelCreateButton.enable();
-
-        if (label.message) {
-          let errors;
-
-          if (typeof label.message === 'string') {
-            errors = label.message;
-          } else {
-            errors = Object.keys(label.message).map(key =>
-              `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
-            ).join("<br/>");
-          }
-
-          this.$newLabelError
-            .html(errors)
-            .show();
-        } else {
-          this.$dropdownBack.trigger('click');
-
-          $(document).trigger('created.label', label);
-        }
-      });
-    }
-  }
-
-  if (!w.gl) {
-    w.gl = {};
-  }
-
-  gl.CreateLabelDropdown = CreateLabelDropdown;
-})(window);
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_code_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
similarity index 89%
rename from app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 8652479e7bf8b4d28aeac0cff78c899b5d9d9a89..42e1bbce7442d433c3815c1a2ea95521e8d09814 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -1,5 +1,6 @@
 /* eslint-disable no-param-reassign */
-/* global Vue */
+import Vue from 'vue';
+import iconCommit from '../svg/icon_commit.svg';
 
 ((global) => {
   global.cycleAnalytics = global.cycleAnalytics || {};
@@ -9,6 +10,11 @@
       items: Array,
       stage: Object,
     },
+
+    data() {
+      return { iconCommit };
+    },
+
     template: `
       <div>
         <div class="events-description">
@@ -31,7 +37,7 @@
               </h5>
               <span>
                 First
-                <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
+                <span class="commit-icon">${iconCommit}</span>
                 <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
                 pushed by
                 <a :href="commit.author.webUrl" class="commit-author-link">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_production_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_review_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
similarity index 88%
rename from app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 82622232f6402322108869f1a683401bddd05307..8fa63734cf13630be61c0cb960fb836714b6dd62 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -1,5 +1,6 @@
 /* eslint-disable no-param-reassign */
-/* global Vue */
+import Vue from 'vue';
+import iconBranch from '../svg/icon_branch.svg';
 
 ((global) => {
   global.cycleAnalytics = global.cycleAnalytics || {};
@@ -9,6 +10,9 @@
       items: Array,
       stage: Object,
     },
+    data() {
+      return { iconBranch };
+    },
     template: `
       <div>
         <div class="events-description">
@@ -22,7 +26,7 @@
                 <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
                 <i class="fa fa-code-fork"></i>
                 <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
-                <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+                <span class="icon-branch">${iconBranch}</span>
                 <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
               </h5>
               <span>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
similarity index 80%
rename from app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 4bfd363a1f126b03084442ecf5801156a6a59f0e..0015249cfaa96c457a70aab21a85395a842aa76d 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -1,5 +1,7 @@
 /* eslint-disable no-param-reassign */
-/* global Vue */
+import Vue from 'vue';
+import iconBuildStatus from '../svg/icon_build_status.svg';
+import iconBranch from '../svg/icon_branch.svg';
 
 ((global) => {
   global.cycleAnalytics = global.cycleAnalytics || {};
@@ -9,6 +11,9 @@
       items: Array,
       stage: Object,
     },
+    data() {
+      return { iconBuildStatus, iconBranch };
+    },
     template: `
       <div>
         <div class="events-description">
@@ -18,13 +23,13 @@
           <li v-for="build in items" class="stage-event-item item-build-component">
             <div class="item-details">
               <h5 class="item-title">
-                <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
+                <span class="icon-build-status">${iconBuildStatus}</span>
                 <a :href="build.url" class="item-build-name">{{ build.name }}</a>
                 &middot;
                 <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
                 <i class="fa fa-code-fork"></i>
                 <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
-                <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+                <span class="icon-branch">${iconBranch}</span>
                 <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
               </h5>
               <span>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6
rename to app/assets/javascripts/cycle_analytics/components/total_time_component.js
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
similarity index 88%
rename from app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
rename to app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 1ac715aab7706429f4db7d7bdddcac5dc12249a2..beff293b587e9523b7ad8ed4b05ccfdd2aed056d 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -4,10 +4,17 @@
 
 window.Vue = require('vue');
 window.Cookies = require('js-cookie');
-
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
+require('./components/stage_code_component');
+require('./components/stage_issue_component');
+require('./components/stage_plan_component');
+require('./components/stage_production_component');
+require('./components/stage_review_component');
+require('./components/stage_staging_component');
+require('./components/stage_test_component');
+require('./components/total_time_component');
+require('./cycle_analytics_service');
+require('./cycle_analytics_store');
+require('./default_event_objects');
 
 $(() => {
   const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
rename to app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
similarity index 95%
rename from app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
rename to app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 3efeb14100854e1cfb667eb16a0610a10a3177ff..7ae9de7297c680b201ebc25269a81674d93c9a07 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -75,8 +75,11 @@ const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
         const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
 
         eventItem.totalTime = eventItem.total_time;
-        eventItem.author.webUrl = eventItem.author.web_url;
-        eventItem.author.avatarUrl = eventItem.author.avatar_url;
+
+        if (eventItem.author) {
+          eventItem.author.webUrl = eventItem.author.web_url;
+          eventItem.author.avatarUrl = eventItem.author.avatar_url;
+        }
 
         if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
         if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js
similarity index 100%
rename from app/assets/javascripts/cycle_analytics/default_event_objects.js.es6
rename to app/assets/javascripts/cycle_analytics/default_event_objects.js
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
deleted file mode 100644
index 5d486bcaf6671e537aff91649504d8a105e3e4e3..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
-  global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
-  global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9f547d3d74432872b045767486a4796b151fdd75
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
deleted file mode 100644
index 661bf9e9f1cf224cfce67e8fbecd308c70eb0047..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
-  global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
-  global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b932d90618a365f07f7b91764828dbe9cbcb8209
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
deleted file mode 100644
index 2208c27a619aecbc6220217f067feb83b92514a8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
-  global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
-  global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6a517756058f04b983c8b92eee1b0670b7e5f651
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
new file mode 100644
index 0000000000000000000000000000000000000000..cfa60325fccb60dfbeaf431f629098497caa22c2
--- /dev/null
+++ b/app/assets/javascripts/diff.js
@@ -0,0 +1,128 @@
+/* eslint-disable class-methods-use-this */
+
+require('./lib/utils/url_utility');
+
+const UNFOLD_COUNT = 20;
+let isBound = false;
+
+class Diff {
+  constructor() {
+    const $diffFile = $('.files .diff-file');
+    $diffFile.singleFileDiff();
+    $diffFile.filesCommentButton();
+
+    $diffFile.each((index, file) => new gl.ImageFile(file));
+
+    if (this.diffViewType() === 'parallel') {
+      $('.content-wrapper .container-fluid').removeClass('container-limited');
+    }
+
+    if (!isBound) {
+      $(document)
+        .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+        .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+      isBound = true;
+    }
+
+    if (gl.utils.getLocationHash()) {
+      this.highlightSelectedLine();
+    }
+
+    this.openAnchoredDiff();
+  }
+
+  handleClickUnfold(e) {
+    const $target = $(e.target);
+    // current babel config relies on iterators implementation, so we cannot simply do:
+    // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
+    const ref = this.lineNumbers($target.parent());
+    const oldLineNumber = ref[0];
+    const newLineNumber = ref[1];
+    const offset = newLineNumber - oldLineNumber;
+    const bottom = $target.hasClass('js-unfold-bottom');
+    let since;
+    let to;
+    let unfold = true;
+
+    if (bottom) {
+      const lineNumber = newLineNumber + 1;
+      since = lineNumber;
+      to = lineNumber + UNFOLD_COUNT;
+    } else {
+      const lineNumber = newLineNumber - 1;
+      since = lineNumber - UNFOLD_COUNT;
+      to = lineNumber;
+
+      // make sure we aren't loading more than we need
+      const prevNewLine = this.lineNumbers($target.parent().prev())[1];
+      if (since <= prevNewLine + 1) {
+        since = prevNewLine + 1;
+        unfold = false;
+      }
+    }
+
+    const file = $target.parents('.diff-file');
+    const link = file.data('blob-diff-path');
+    const view = file.data('view');
+
+    const params = { since, to, bottom, offset, unfold, view };
+    $.get(link, params, response => $target.parent().replaceWith(response));
+  }
+
+  openAnchoredDiff(cb) {
+    const locationHash = gl.utils.getLocationHash();
+    const anchoredDiff = locationHash && locationHash.split('_')[0];
+
+    if (!anchoredDiff) return;
+
+    const diffTitle = $(`#${anchoredDiff}`);
+    const diffFile = diffTitle.closest('.diff-file');
+    const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+    if (nothingHereBlock.length) {
+      const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
+      diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
+        this.highlightSelectedLine();
+        if (cb) cb();
+      });
+    } else if (cb) {
+      cb();
+    }
+  }
+
+  handleClickLineNum(e) {
+    const hash = $(e.currentTarget).attr('href');
+    e.preventDefault();
+    if (window.history.pushState) {
+      window.history.pushState(null, null, hash);
+    } else {
+      window.location.hash = hash;
+    }
+    this.highlightSelectedLine();
+  }
+
+  diffViewType() {
+    return $('.inline-parallel-buttons a.active').data('view-type');
+  }
+
+  lineNumbers(line) {
+    if (!line.children().length) {
+      return [0, 0];
+    }
+    return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+  }
+
+  highlightSelectedLine() {
+    const hash = gl.utils.getLocationHash();
+    const $diffFiles = $('.diff-file');
+    $diffFiles.find('.hll').removeClass('hll');
+
+    if (hash) {
+      $diffFiles
+        .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
+        .addClass('hll');
+    }
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.Diff = Diff;
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
deleted file mode 100644
index ccccd0a36ff966e18e00fbf37e0fab9751f76fe6..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/diff.js.es6
+++ /dev/null
@@ -1,126 +0,0 @@
-/* eslint-disable class-methods-use-this */
-
-require('./lib/utils/url_utility');
-
-(() => {
-  const UNFOLD_COUNT = 20;
-  let isBound = false;
-
-  class Diff {
-    constructor() {
-      const $diffFile = $('.files .diff-file');
-      $diffFile.singleFileDiff();
-      $diffFile.filesCommentButton();
-
-      $diffFile.each((index, file) => new gl.ImageFile(file));
-
-      if (this.diffViewType() === 'parallel') {
-        $('.content-wrapper .container-fluid').removeClass('container-limited');
-      }
-
-      if (!isBound) {
-        $(document)
-          .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
-          .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
-        isBound = true;
-      }
-
-      this.openAnchoredDiff();
-    }
-
-    handleClickUnfold(e) {
-      const $target = $(e.target);
-      // current babel config relies on iterators implementation, so we cannot simply do:
-      // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
-      const ref = this.lineNumbers($target.parent());
-      const oldLineNumber = ref[0];
-      const newLineNumber = ref[1];
-      const offset = newLineNumber - oldLineNumber;
-      const bottom = $target.hasClass('js-unfold-bottom');
-      let since;
-      let to;
-      let unfold = true;
-
-      if (bottom) {
-        const lineNumber = newLineNumber + 1;
-        since = lineNumber;
-        to = lineNumber + UNFOLD_COUNT;
-      } else {
-        const lineNumber = newLineNumber - 1;
-        since = lineNumber - UNFOLD_COUNT;
-        to = lineNumber;
-
-        // make sure we aren't loading more than we need
-        const prevNewLine = this.lineNumbers($target.parent().prev())[1];
-        if (since <= prevNewLine + 1) {
-          since = prevNewLine + 1;
-          unfold = false;
-        }
-      }
-
-      const file = $target.parents('.diff-file');
-      const link = file.data('blob-diff-path');
-      const view = file.data('view');
-
-      const params = { since, to, bottom, offset, unfold, view };
-      $.get(link, params, response => $target.parent().replaceWith(response));
-    }
-
-    openAnchoredDiff(cb) {
-      const locationHash = gl.utils.getLocationHash();
-      const anchoredDiff = locationHash && locationHash.split('_')[0];
-
-      if (!anchoredDiff) return;
-
-      const diffTitle = $(`#${anchoredDiff}`);
-      const diffFile = diffTitle.closest('.diff-file');
-      const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
-      if (nothingHereBlock.length) {
-        const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
-        diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
-          this.highlighSelectedLine();
-          if (cb) cb();
-        });
-      } else if (cb) {
-        cb();
-      }
-    }
-
-    handleClickLineNum(e) {
-      const hash = $(e.currentTarget).attr('href');
-      e.preventDefault();
-      if (window.history.pushState) {
-        window.history.pushState(null, null, hash);
-      } else {
-        window.location.hash = hash;
-      }
-      this.highlighSelectedLine();
-    }
-
-    diffViewType() {
-      return $('.inline-parallel-buttons a.active').data('view-type');
-    }
-
-    lineNumbers(line) {
-      if (!line.children().length) {
-        return [0, 0];
-      }
-      return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
-    }
-
-    highlighSelectedLine() {
-      const hash = gl.utils.getLocationHash();
-      const $diffFiles = $('.diff-file');
-      $diffFiles.find('.hll').removeClass('hll');
-
-      if (hash) {
-        $diffFiles
-          .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
-          .addClass('hll');
-      }
-    }
-  }
-
-  window.gl = window.gl || {};
-  window.gl.Diff = Diff;
-})();
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
similarity index 100%
rename from app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
rename to app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd7081aefb7bb79bf1f26ff87ea7ee6f04add0a0
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -0,0 +1,156 @@
+/* global CommentsStore Cookies notes */
+import Vue from 'vue';
+import collapseIcon from '../icons/collapse_icon.svg';
+
+(() => {
+  const DiffNoteAvatars = Vue.extend({
+    props: ['discussionId'],
+    data() {
+      return {
+        isVisible: false,
+        lineType: '',
+        storeState: CommentsStore.state,
+        shownAvatars: 3,
+        collapseIcon,
+      };
+    },
+    template: `
+      <div class="diff-comment-avatar-holders"
+        v-show="notesCount !== 0">
+        <div v-if="!isVisible">
+          <img v-for="note in notesSubset"
+            class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
+            width="19"
+            height="19"
+            role="button"
+            data-container="body"
+            data-placement="top"
+            data-html="true"
+            :data-line-type="lineType"
+            :title="note.authorName + ': ' + note.noteTruncated"
+            :src="note.authorAvatar"
+            @click="clickedAvatar($event)" />
+          <span v-if="notesCount > shownAvatars"
+            class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+            data-container="body"
+            data-placement="top"
+            ref="extraComments"
+            role="button"
+            :data-line-type="lineType"
+            :title="extraNotesTitle"
+            @click="clickedAvatar($event)">{{ moreText }}</span>
+        </div>
+        <button class="diff-notes-collapse js-diff-comment-avatar"
+          type="button"
+          aria-label="Show comments"
+          :data-line-type="lineType"
+          @click="clickedAvatar($event)"
+          v-if="isVisible"
+          v-html="collapseIcon">
+        </button>
+      </div>
+    `,
+    mounted() {
+      this.$nextTick(() => {
+        this.addNoCommentClass();
+        this.setDiscussionVisible();
+
+        this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+      });
+
+      $(document).on('toggle.comments', () => {
+        this.$nextTick(() => {
+          this.setDiscussionVisible();
+        });
+      });
+    },
+    destroyed() {
+      $(document).off('toggle.comments');
+    },
+    watch: {
+      storeState: {
+        handler() {
+          this.$nextTick(() => {
+            $('.has-tooltip', this.$el).tooltip('fixTitle');
+
+            // We need to add/remove a class to an element that is outside the Vue instance
+            this.addNoCommentClass();
+          });
+        },
+        deep: true,
+      },
+    },
+    computed: {
+      notesSubset() {
+        let notes = [];
+
+        if (this.discussion) {
+          notes = Object.keys(this.discussion.notes)
+            .slice(0, this.shownAvatars)
+            .map(noteId => this.discussion.notes[noteId]);
+        }
+
+        return notes;
+      },
+      extraNotesTitle() {
+        if (this.discussion) {
+          const extra = this.discussion.notesCount() - this.shownAvatars;
+
+          return `${extra} more comment${extra > 1 ? 's' : ''}`;
+        }
+
+        return '';
+      },
+      discussion() {
+        return this.storeState[this.discussionId];
+      },
+      notesCount() {
+        if (this.discussion) {
+          return this.discussion.notesCount();
+        }
+
+        return 0;
+      },
+      moreText() {
+        const plusSign = this.notesCount < 100 ? '+' : '';
+
+        return `${plusSign}${this.notesCount - this.shownAvatars}`;
+      },
+    },
+    methods: {
+      clickedAvatar(e) {
+        notes.addDiffNote(e);
+
+        // Toggle the active state of the toggle all button
+        this.toggleDiscussionsToggleState();
+
+        this.$nextTick(() => {
+          this.setDiscussionVisible();
+
+          $('.has-tooltip', this.$el).tooltip('fixTitle');
+          $('.has-tooltip', this.$el).tooltip('hide');
+        });
+      },
+      addNoCommentClass() {
+        const notesCount = this.notesCount;
+
+        $(this.$el).closest('.js-avatar-container')
+          .toggleClass('js-no-comment-btn', notesCount > 0)
+          .nextUntil('.js-avatar-container')
+          .toggleClass('js-no-comment-btn', notesCount > 0);
+      },
+      toggleDiscussionsToggleState() {
+        const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
+        const $visibleNotesHolders = $notesHolders.filter(':visible');
+        const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
+
+        $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
+      },
+      setDiscussionVisible() {
+        this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
+      },
+    },
+  });
+
+  Vue.component('diff-note-avatars', DiffNoteAvatars);
+})();
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
similarity index 100%
rename from app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
rename to app/assets/javascripts/diff_notes/components/jump_to_discussion.js
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
new file mode 100644
index 0000000000000000000000000000000000000000..e86bef471722ad62b722c4272c69f0ffd9288b8b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -0,0 +1,29 @@
+/* global Vue */
+/* global CommentsStore */
+
+(() => {
+  const NewIssueForDiscussion = Vue.extend({
+    props: {
+      discussionId: {
+        type: String,
+        required: true,
+      },
+    },
+    data() {
+      return {
+        discussions: CommentsStore.state,
+      };
+    },
+    computed: {
+      discussion() {
+        return this.discussions[this.discussionId];
+      },
+      showButton() {
+        if (this.discussion) return !this.discussion.isResolved();
+        return false;
+      },
+    },
+  });
+
+  Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js
similarity index 85%
rename from app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
rename to app/assets/javascripts/diff_notes/components/resolve_btn.js
index d1873d6c7a23ad5b69b643d89d5aeb2538e31964..fbd980f0fcee971892b8fcfc13de50fbd1d44c7d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -11,7 +11,10 @@ const Vue = require('vue');
       discussionId: String,
       resolved: Boolean,
       canResolve: Boolean,
-      resolvedBy: String
+      resolvedBy: String,
+      authorName: String,
+      authorAvatar: String,
+      noteTruncated: String,
     },
     data: function () {
       return {
@@ -98,7 +101,16 @@ const Vue = require('vue');
       CommentsStore.delete(this.discussionId, this.noteId);
     },
     created: function () {
-      CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+      CommentsStore.create({
+        discussionId: this.discussionId,
+        noteId: this.noteId,
+        canResolve: this.canResolve,
+        resolved: this.resolved,
+        resolvedBy: this.resolvedBy,
+        authorName: this.authorName,
+        authorAvatar: this.authorAvatar,
+        noteTruncated: this.noteTruncated,
+      });
 
       this.note = this.discussion.getNote(this.noteId);
     }
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js
similarity index 100%
rename from app/assets/javascripts/diff_notes/components/resolve_count.js.es6
rename to app/assets/javascripts/diff_notes/components/resolve_count.js
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
similarity index 100%
rename from app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
rename to app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
similarity index 57%
rename from app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
rename to app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 190461451d508e9944a61b7d16022bf5571ac3dd..4f6b86a917cd09c5a6a8016f6ea59e860cbdc0d0 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,18 +1,24 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
 /* global Vue */
 /* global ResolveCount */
 
-function requireAll(context) { return context.keys().map(context); }
 const Vue = require('vue');
-requireAll(require.context('./models',     false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores',     false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services',   false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins',     false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
+require('./models/discussion');
+require('./models/note');
+require('./stores/comments');
+require('./services/resolve');
+require('./mixins/discussion');
+require('./components/comment_resolve_btn');
+require('./components/jump_to_discussion');
+require('./components/resolve_btn');
+require('./components/resolve_count');
+require('./components/resolve_discussion_btn');
+require('./components/diff_note_avatars');
+require('./components/new_issue_for_discussion');
 
 $(() => {
   const projectPath = document.querySelector('.merge-request').dataset.projectPath;
-  const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
+  const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
 
   window.gl = window.gl || {};
   window.gl.diffNoteApps = {};
@@ -20,6 +26,15 @@ $(() => {
   window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
 
   gl.diffNotesCompileComponents = () => {
+    $('diff-note-avatars').each(function () {
+      const tmp = Vue.extend({
+        template: $(this).get(0).outerHTML
+      });
+      const tmpApp = new tmp().$mount();
+
+      $(this).replaceWith(tmpApp.$el);
+    });
+
     const $components = $(COMPONENT_SELECTOR).filter(function () {
       return $(this).closest('resolve-count').length !== 1;
     });
diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bd4b393cfaadb4f4eb912eabe1ee3e8363d76747
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
@@ -0,0 +1 @@
+<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js
similarity index 100%
rename from app/assets/javascripts/diff_notes/mixins/discussion.js.es6
rename to app/assets/javascripts/diff_notes/mixins/discussion.js
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js
similarity index 92%
rename from app/assets/javascripts/diff_notes/models/discussion.js.es6
rename to app/assets/javascripts/diff_notes/models/discussion.js
index fa518ba4d33a8b47c8d6f40b073e59538565c155..dce1a9b58bd92836ebc0078fdccd6a0b0f1c0b24 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -10,8 +10,8 @@ class DiscussionModel {
     this.canResolve = false;
   }
 
-  createNote (noteId, canResolve, resolved, resolved_by) {
-    Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+  createNote (noteObj) {
+    Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
   }
 
   deleteNote (noteId) {
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
new file mode 100644
index 0000000000000000000000000000000000000000..04465aa507e431a267bf66e96ef207f4f7675aea
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js
@@ -0,0 +1,16 @@
+/* eslint-disable camelcase, no-unused-vars */
+
+class NoteModel {
+  constructor(discussionId, noteObj) {
+    this.discussionId = discussionId;
+    this.id = noteObj.noteId;
+    this.canResolve = noteObj.canResolve;
+    this.resolved = noteObj.resolved;
+    this.resolved_by = noteObj.resolvedBy;
+    this.authorName = noteObj.authorName;
+    this.authorAvatar = noteObj.authorAvatar;
+    this.noteTruncated = noteObj.noteTruncated;
+  }
+}
+
+window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
deleted file mode 100644
index f3a7cba5ef6bb331a14d595a9475971435c2ad4d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-/* eslint-disable camelcase, no-unused-vars */
-
-class NoteModel {
-  constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
-    this.discussionId = discussionId;
-    this.id = noteId;
-    this.canResolve = canResolve;
-    this.resolved = resolved;
-    this.resolved_by = resolved_by;
-  }
-}
-
-window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js
similarity index 100%
rename from app/assets/javascripts/diff_notes/services/resolve.js.es6
rename to app/assets/javascripts/diff_notes/services/resolve.js
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js
similarity index 87%
rename from app/assets/javascripts/diff_notes/stores/comments.js.es6
rename to app/assets/javascripts/diff_notes/stores/comments.js
index c80d979b977ded586dff8c1f303286b50c003d8b..69c4d7a8434a48fc9b54d363f4217cd9cb2619fd 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -21,10 +21,10 @@
 
       return discussion;
     },
-    create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
-      const discussion = this.createDiscussion(discussionId);
+    create: function (noteObj) {
+      const discussion = this.createDiscussion(noteObj.discussionId);
 
-      discussion.createNote(noteId, canResolve, resolved, resolved_by);
+      discussion.createNote(noteObj);
     },
     update: function (discussionId, noteId, resolved, resolved_by) {
       const discussion = this.state[discussionId];
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js
similarity index 81%
rename from app/assets/javascripts/dispatcher.js.es6
rename to app/assets/javascripts/dispatcher.js
index f55db02f0fd2b4f1c4db1a756bfdd455c45a7ccf..3557f6f617ed64dcbcf7c21a09e7e4a1aa0774ee 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js
@@ -4,7 +4,6 @@
 /* global ShortcutsNavigation */
 /* global Build */
 /* global Issuable */
-/* global Issue */
 /* global ShortcutsIssuable */
 /* global ZenMode */
 /* global Milestone */
@@ -34,8 +33,17 @@
 /* global ProjectShow */
 /* global Labels */
 /* global Shortcuts */
+import Issue from './issue';
+
+import BindInOut from './behaviors/bind_in_out';
+import GroupName from './group_name';
+import GroupsList from './groups_list';
+import ProjectsList from './projects_list';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
+import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
 
 const ShortcutsBlob = require('./shortcuts_blob');
+const UserCallout = require('./user_callout');
 
 (function() {
   var Dispatcher;
@@ -52,13 +60,32 @@ const ShortcutsBlob = require('./shortcuts_blob');
     }
 
     Dispatcher.prototype.initPageScripts = function() {
-      var page, path, shortcut_handler;
+      var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
       page = $('body').attr('data-page');
       if (!page) {
         return false;
       }
       path = page.split(':');
       shortcut_handler = null;
+
+      function initBlob() {
+        new LineHighlighter();
+
+        new BlobLinePermalinkUpdater(
+          document.querySelector('#blob-content-holder'),
+          '.diff-line-num[data-line-number]',
+          document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+        );
+
+        shortcut_handler = new ShortcutsNavigation();
+        fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+        fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+        new ShortcutsBlob({
+          skipResetBindings: true,
+          fileBlobPermalinkUrl,
+        });
+      }
+
       switch (page) {
         case 'sessions:new':
           new UsernameValidator();
@@ -95,6 +122,18 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'dashboard:todos:index':
           new gl.Todos();
           break;
+        case 'dashboard:projects:index':
+        case 'dashboard:projects:starred':
+        case 'explore:projects:index':
+        case 'explore:projects:trending':
+        case 'explore:projects:starred':
+        case 'admin:projects:index':
+          new ProjectsList();
+          break;
+        case 'dashboard:groups:index':
+        case 'explore:groups:index':
+          new GroupsList();
+          break;
         case 'projects:milestones:new':
         case 'projects:milestones:edit':
         case 'projects:milestones:update':
@@ -108,6 +147,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'projects:compare:show':
           new gl.Diff();
           break;
+        case 'projects:branches:index':
+          gl.AjaxLoadingSpinner.init();
+          break;
         case 'projects:issues:new':
         case 'projects:issues:edit':
           shortcut_handler = new ShortcutsNavigation();
@@ -153,18 +195,18 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'dashboard:activity':
           new gl.Activities();
           break;
-        case 'dashboard:projects:starred':
-          new gl.Activities();
-          break;
         case 'projects:commit:show':
           new Commit();
           new gl.Diff();
           new ZenMode();
           shortcut_handler = new ShortcutsNavigation();
+          new MiniPipelineGraph({
+            container: '.js-commit-pipeline-graph',
+          }).bindEvents();
           break;
         case 'projects:commit:pipelines':
-          new gl.MiniPipelineGraph({
-            container: '.js-pipeline-table',
+          new MiniPipelineGraph({
+            container: '.js-commit-pipeline-graph',
           }).bindEvents();
           break;
         case 'projects:commits:show':
@@ -198,6 +240,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
           shortcut_handler = new ShortcutsNavigation();
           new NotificationsForm();
           new NotificationsDropdown();
+          new ProjectsList();
           break;
         case 'groups:group_members:index':
           new gl.MemberExpirationDate();
@@ -212,28 +255,39 @@ const ShortcutsBlob = require('./shortcuts_blob');
           new UsersSelect();
           break;
         case 'groups:new':
+        case 'admin:groups:new':
+        case 'groups:create':
+        case 'admin:groups:create':
+          BindInOut.initAll();
+        case 'groups:new':
+        case 'admin:groups:new':
         case 'groups:edit':
         case 'admin:groups:edit':
-        case 'admin:groups:new':
           new GroupAvatar();
           break;
         case 'projects:tree:show':
           shortcut_handler = new ShortcutsNavigation();
           new TreeView();
+          gl.TargetBranchDropDown.bootstrap();
           break;
         case 'projects:find_file:show':
           shortcut_handler = true;
           break;
+        case 'projects:blob:new':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
+        case 'projects:blob:create':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
         case 'projects:blob:show':
+          gl.TargetBranchDropDown.bootstrap();
+          initBlob();
+          break;
+        case 'projects:blob:edit':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
         case 'projects:blame:show':
-          new LineHighlighter();
-          shortcut_handler = new ShortcutsNavigation();
-          const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
-          const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
-          new ShortcutsBlob({
-            skipResetBindings: true,
-            fileBlobPermalinkUrl,
-          });
+          initBlob();
           break;
         case 'groups:labels:new':
         case 'groups:labels:edit':
@@ -263,7 +317,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'search:show':
           new Search();
           break;
-        case 'projects:protected_branches:index':
+        case 'projects:repository:show':
           new gl.ProtectedBranchCreate();
           new gl.ProtectedBranchEditList();
           break;
@@ -274,6 +328,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'ci:lints:show':
           new gl.CILintEditor();
           break;
+        case 'users:show':
+          new UserCallout();
+          break;
       }
       switch (path.first()) {
         case 'sessions':
@@ -310,6 +367,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'dashboard':
         case 'root':
           shortcut_handler = new ShortcutsDashboardNavigation();
+          new UserCallout();
+          break;
+        case 'groups':
+          new GroupName();
           break;
         case 'profiles':
           new NotificationsForm();
@@ -318,6 +379,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
         case 'projects':
           new Project();
           new ProjectAvatar();
+          new GroupName();
           switch (path[1]) {
             case 'compare':
               new CompareAutocomplete();
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index 5cdf11c6a2c52be01bad68cc9a870fe2c4fd4f31..020f8b4ac6567fe2af64d86c1e4d37a6575cee8b 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -37,11 +37,14 @@ require('../window')(function(w){
         }
       }
 
-      self.hook.list[config.method].call(self.hook.list, data);
+      if (!self.destroyed) {
+        self.hook.list[config.method].call(self.hook.list, data);
+      }
     },
 
     init: function init(hook) {
       var self = this;
+      self.destroyed = false;
       self.cache = self.cache || {};
       var config = hook.config.droplabAjax;
       this.hook = hook;
@@ -71,6 +74,9 @@ require('../window')(function(w){
         this._loadUrlData(config.endpoint)
           .then(function(d) {
             self._loadData(d, config, self);
+          }, function(xhrError) {
+            // TODO: properly handle errors due to XHR cancellation
+            return;
           }).catch(function(e) {
             throw new droplabAjaxException(e.message || e);
           });
@@ -79,6 +85,7 @@ require('../window')(function(w){
 
     destroy: function() {
       var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+      this.destroyed = true;
       if (this.listTemplate && dynamicList) {
         dynamicList.outerHTML = this.listTemplate;
       }
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index b63d73066cb6717383bdc4ef08b81678fdd87ba1..05eba7aef56064dd0ab0887c16876d9441bed0d2 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -82,6 +82,9 @@ require('../window')(function(w){
         this._loadUrlData(url)
           .then(function(data) {
             self._loadData(data, config, self);
+          }, function(xhrError) {
+            // TODO: properly handle errors due to XHR cancellation
+            return;
           });
       }
     },
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 64a7a9eaf3736ef01ab9646d907793326511097d..f2963a5eb190c3f3874ca8e86a465d45f0abb33e 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -3,217 +3,216 @@
 
 require('./preview_markdown');
 
-(function() {
-  this.DropzoneInput = (function() {
-    function DropzoneInput(form) {
-      var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
-      Dropzone.autoDiscover = false;
-      alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
-      alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
-      divHover = "<div class=\"div-dropzone-hover\"></div>";
-      divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
-      divAlert = "<div class=\"" + alertClass + "\"></div>";
-      iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
-      iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
-      uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
-      btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
-      project_uploads_path = window.project_uploads_path || null;
-      max_file_size = gon.max_file_size || 10;
-      form_textarea = $(form).find(".js-gfm-input");
-      form_textarea.wrap("<div class=\"div-dropzone\"></div>");
-      form_textarea.on('paste', (function(_this) {
-        return function(event) {
-          return handlePaste(event);
-        };
-      })(this));
-      $mdArea = $(form_textarea).closest('.md-area');
-      $(form).setupMarkdownPreview();
-      form_dropzone = $(form).find('.div-dropzone');
-      form_dropzone.parent().addClass("div-dropzone-wrapper");
-      form_dropzone.append(divHover);
-      form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
-      form_dropzone.append(divSpinner);
-      form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
-      form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
-      form_dropzone.find(".div-dropzone-spinner").css({
-        "opacity": 0,
-        "display": "none"
-      });
-      dropzone = form_dropzone.dropzone({
-        url: project_uploads_path,
-        dictDefaultMessage: "",
-        clickable: true,
-        paramName: "file",
-        maxFilesize: max_file_size,
-        uploadMultiple: false,
-        headers: {
-          "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
-        },
-        previewContainer: false,
-        processing: function() {
-          return $(".div-dropzone-alert").alert("close");
-        },
-        dragover: function() {
-          $mdArea.addClass('is-dropzone-hover');
-          form.find(".div-dropzone-hover").css("opacity", 0.7);
-        },
-        dragleave: function() {
-          $mdArea.removeClass('is-dropzone-hover');
-          form.find(".div-dropzone-hover").css("opacity", 0);
-        },
-        drop: function() {
-          $mdArea.removeClass('is-dropzone-hover');
-          form.find(".div-dropzone-hover").css("opacity", 0);
-          form_textarea.focus();
-        },
-        success: function(header, response) {
-          pasteText(response.link.markdown);
-        },
-        error: function(temp) {
-          var checkIfMsgExists, errorAlert;
-          errorAlert = $(form).find('.error-alert');
-          checkIfMsgExists = errorAlert.children().length;
-          if (checkIfMsgExists === 0) {
-            errorAlert.append(divAlert);
-            $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
-          }
-        },
-        totaluploadprogress: function(totalUploadProgress) {
-          uploadProgress.text(Math.round(totalUploadProgress) + "%");
-        },
-        sending: function() {
-          form_dropzone.find(".div-dropzone-spinner").css({
-            "opacity": 0.7,
-            "display": "inherit"
-          });
-        },
-        queuecomplete: function() {
-          uploadProgress.text("");
-          $(".dz-preview").remove();
-          $(".markdown-area").trigger("input");
-          $(".div-dropzone-spinner").css({
-            "opacity": 0,
-            "display": "none"
-          });
-        }
-      });
-      child = $(dropzone[0]).children("textarea");
-      handlePaste = function(event) {
-        var filename, image, pasteEvent, text;
-        pasteEvent = event.originalEvent;
-        if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
-          image = isImage(pasteEvent);
-          if (image) {
-            event.preventDefault();
-            filename = getFilename(pasteEvent) || "image.png";
-            text = "{{" + filename + "}}";
-            pasteText(text);
-            return uploadFile(image.getAsFile(), filename);
-          }
-        }
+window.DropzoneInput = (function() {
+  function DropzoneInput(form) {
+    var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+    Dropzone.autoDiscover = false;
+    alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
+    alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
+    divHover = "<div class=\"div-dropzone-hover\"></div>";
+    divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
+    divAlert = "<div class=\"" + alertClass + "\"></div>";
+    iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
+    iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
+    uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
+    btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
+    project_uploads_path = window.project_uploads_path || null;
+    max_file_size = gon.max_file_size || 10;
+    form_textarea = $(form).find(".js-gfm-input");
+    form_textarea.wrap("<div class=\"div-dropzone\"></div>");
+    form_textarea.on('paste', (function(_this) {
+      return function(event) {
+        return handlePaste(event);
       };
-      isImage = function(data) {
-        var i, item;
-        i = 0;
-        while (i < data.clipboardData.items.length) {
-          item = data.clipboardData.items[i];
-          if (item.type.indexOf("image") !== -1) {
-            return item;
-          }
-          i += 1;
-        }
-        return false;
-      };
-      pasteText = function(text) {
-        var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
-        caretStart = $(child)[0].selectionStart;
-        caretEnd = $(child)[0].selectionEnd;
-        textEnd = $(child).val().length;
-        beforeSelection = $(child).val().substring(0, caretStart);
-        afterSelection = $(child).val().substring(caretEnd, textEnd);
-        $(child).val(beforeSelection + text + afterSelection);
-        child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length);
-        return form_textarea.trigger("input");
-      };
-      getFilename = function(e) {
-        var value;
-        if (window.clipboardData && window.clipboardData.getData) {
-          value = window.clipboardData.getData("Text");
-        } else if (e.clipboardData && e.clipboardData.getData) {
-          value = e.clipboardData.getData("text/plain");
+    })(this));
+    $mdArea = $(form_textarea).closest('.md-area');
+    $(form).setupMarkdownPreview();
+    form_dropzone = $(form).find('.div-dropzone');
+    form_dropzone.parent().addClass("div-dropzone-wrapper");
+    form_dropzone.append(divHover);
+    form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
+    form_dropzone.append(divSpinner);
+    form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
+    form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
+    form_dropzone.find(".div-dropzone-spinner").css({
+      "opacity": 0,
+      "display": "none"
+    });
+    dropzone = form_dropzone.dropzone({
+      url: project_uploads_path,
+      dictDefaultMessage: "",
+      clickable: true,
+      paramName: "file",
+      maxFilesize: max_file_size,
+      uploadMultiple: false,
+      headers: {
+        "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+      },
+      previewContainer: false,
+      processing: function() {
+        return $(".div-dropzone-alert").alert("close");
+      },
+      dragover: function() {
+        $mdArea.addClass('is-dropzone-hover');
+        form.find(".div-dropzone-hover").css("opacity", 0.7);
+      },
+      dragleave: function() {
+        $mdArea.removeClass('is-dropzone-hover');
+        form.find(".div-dropzone-hover").css("opacity", 0);
+      },
+      drop: function() {
+        $mdArea.removeClass('is-dropzone-hover');
+        form.find(".div-dropzone-hover").css("opacity", 0);
+        form_textarea.focus();
+      },
+      success: function(header, response) {
+        pasteText(response.link.markdown);
+      },
+      error: function(temp) {
+        var checkIfMsgExists, errorAlert;
+        errorAlert = $(form).find('.error-alert');
+        checkIfMsgExists = errorAlert.children().length;
+        if (checkIfMsgExists === 0) {
+          errorAlert.append(divAlert);
+          $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
         }
-        value = value.split("\r");
-        return value.first();
-      };
-      uploadFile = function(item, filename) {
-        var formData;
-        formData = new FormData();
-        formData.append("file", item, filename);
-        return $.ajax({
-          url: project_uploads_path,
-          type: "POST",
-          data: formData,
-          dataType: "json",
-          processData: false,
-          contentType: false,
-          headers: {
-            "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
-          },
-          beforeSend: function() {
-            showSpinner();
-            return closeAlertMessage();
-          },
-          success: function(e, textStatus, response) {
-            return insertToTextArea(filename, response.responseJSON.link.markdown);
-          },
-          error: function(response) {
-            return showError(response.responseJSON.message);
-          },
-          complete: function() {
-            return closeSpinner();
-          }
-        });
-      };
-      insertToTextArea = function(filename, url) {
-        return $(child).val(function(index, val) {
-          return val.replace("{{" + filename + "}}", url + "\n");
-        });
-      };
-      appendToTextArea = function(url) {
-        return $(child).val(function(index, val) {
-          return val + url + "\n";
-        });
-      };
-      showSpinner = function(e) {
-        return form.find(".div-dropzone-spinner").css({
+      },
+      totaluploadprogress: function(totalUploadProgress) {
+        uploadProgress.text(Math.round(totalUploadProgress) + "%");
+      },
+      sending: function() {
+        form_dropzone.find(".div-dropzone-spinner").css({
           "opacity": 0.7,
           "display": "inherit"
         });
-      };
-      closeSpinner = function() {
-        return form.find(".div-dropzone-spinner").css({
+      },
+      queuecomplete: function() {
+        uploadProgress.text("");
+        $(".dz-preview").remove();
+        $(".markdown-area").trigger("input");
+        $(".div-dropzone-spinner").css({
           "opacity": 0,
           "display": "none"
         });
-      };
-      showError = function(message) {
-        var checkIfMsgExists, errorAlert;
-        errorAlert = $(form).find('.error-alert');
-        checkIfMsgExists = errorAlert.children().length;
-        if (checkIfMsgExists === 0) {
-          errorAlert.append(divAlert);
-          return $(".div-dropzone-alert").append(btnAlert + message);
+      }
+    });
+    child = $(dropzone[0]).children("textarea");
+    handlePaste = function(event) {
+      var filename, image, pasteEvent, text;
+      pasteEvent = event.originalEvent;
+      if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
+        image = isImage(pasteEvent);
+        if (image) {
+          event.preventDefault();
+          filename = getFilename(pasteEvent) || "image.png";
+          text = "{{" + filename + "}}";
+          pasteText(text);
+          return uploadFile(image.getAsFile(), filename);
         }
-      };
-      closeAlertMessage = function() {
-        return form.find(".div-dropzone-alert").alert("close");
-      };
-      form.find(".markdown-selector").click(function(e) {
-        e.preventDefault();
-        $(this).closest('.gfm-form').find('.div-dropzone').click();
+      }
+    };
+    isImage = function(data) {
+      var i, item;
+      i = 0;
+      while (i < data.clipboardData.items.length) {
+        item = data.clipboardData.items[i];
+        if (item.type.indexOf("image") !== -1) {
+          return item;
+        }
+        i += 1;
+      }
+      return false;
+    };
+    pasteText = function(text) {
+      var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
+      var formattedText = text + "\n\n";
+      caretStart = $(child)[0].selectionStart;
+      caretEnd = $(child)[0].selectionEnd;
+      textEnd = $(child).val().length;
+      beforeSelection = $(child).val().substring(0, caretStart);
+      afterSelection = $(child).val().substring(caretEnd, textEnd);
+      $(child).val(beforeSelection + formattedText + afterSelection);
+      child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+      return form_textarea.trigger("input");
+    };
+    getFilename = function(e) {
+      var value;
+      if (window.clipboardData && window.clipboardData.getData) {
+        value = window.clipboardData.getData("Text");
+      } else if (e.clipboardData && e.clipboardData.getData) {
+        value = e.clipboardData.getData("text/plain");
+      }
+      value = value.split("\r");
+      return value.first();
+    };
+    uploadFile = function(item, filename) {
+      var formData;
+      formData = new FormData();
+      formData.append("file", item, filename);
+      return $.ajax({
+        url: project_uploads_path,
+        type: "POST",
+        data: formData,
+        dataType: "json",
+        processData: false,
+        contentType: false,
+        headers: {
+          "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+        },
+        beforeSend: function() {
+          showSpinner();
+          return closeAlertMessage();
+        },
+        success: function(e, textStatus, response) {
+          return insertToTextArea(filename, response.responseJSON.link.markdown);
+        },
+        error: function(response) {
+          return showError(response.responseJSON.message);
+        },
+        complete: function() {
+          return closeSpinner();
+        }
+      });
+    };
+    insertToTextArea = function(filename, url) {
+      return $(child).val(function(index, val) {
+        return val.replace("{{" + filename + "}}", url + "\n");
+      });
+    };
+    appendToTextArea = function(url) {
+      return $(child).val(function(index, val) {
+        return val + url + "\n";
+      });
+    };
+    showSpinner = function(e) {
+      return form.find(".div-dropzone-spinner").css({
+        "opacity": 0.7,
+        "display": "inherit"
+      });
+    };
+    closeSpinner = function() {
+      return form.find(".div-dropzone-spinner").css({
+        "opacity": 0,
+        "display": "none"
       });
-    }
+    };
+    showError = function(message) {
+      var checkIfMsgExists, errorAlert;
+      errorAlert = $(form).find('.error-alert');
+      checkIfMsgExists = errorAlert.children().length;
+      if (checkIfMsgExists === 0) {
+        errorAlert.append(divAlert);
+        return $(".div-dropzone-alert").append(btnAlert + message);
+      }
+    };
+    closeAlertMessage = function() {
+      return form.find(".div-dropzone-alert").alert("close");
+    };
+    form.find(".markdown-selector").click(function(e) {
+      e.preventDefault();
+      $(this).closest('.gfm-form').find('.div-dropzone').click();
+    });
+  }
 
-    return DropzoneInput;
-  })();
-}).call(window);
+  return DropzoneInput;
+})();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
new file mode 100644
index 0000000000000000000000000000000000000000..db10b3839138eae4b4f566cd296602a3a572ca0b
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js
@@ -0,0 +1,203 @@
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
+/* global dateFormat */
+/* global Pikaday */
+
+class DueDateSelect {
+  constructor({ $dropdown, $loading } = {}) {
+    const $dropdownParent = $dropdown.closest('.dropdown');
+    const $block = $dropdown.closest('.block');
+    this.$loading = $loading;
+    this.$dropdown = $dropdown;
+    this.$dropdownParent = $dropdownParent;
+    this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
+    this.$block = $block;
+    this.$selectbox = $dropdown.closest('.selectbox');
+    this.$value = $block.find('.value');
+    this.$valueContent = $block.find('.value-content');
+    this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
+    this.fieldName = $dropdown.data('field-name'),
+    this.abilityName = $dropdown.data('ability-name'),
+    this.issueUpdateURL = $dropdown.data('issue-update');
+
+    this.rawSelectedDate = null;
+    this.displayedDate = null;
+    this.datePayload = null;
+
+    this.initGlDropdown();
+    this.initRemoveDueDate();
+    this.initDatePicker();
+  }
+
+  initGlDropdown() {
+    this.$dropdown.glDropdown({
+      opened: () => {
+        const calendar = this.$datePicker.data('pikaday');
+        calendar.show();
+      },
+      hidden: () => {
+        this.$selectbox.hide();
+        this.$value.css('display', '');
+      }
+    });
+  }
+
+  initDatePicker() {
+    const $dueDateInput = $(`input[name='${this.fieldName}']`);
+
+    const calendar = new Pikaday({
+      field: $dueDateInput.get(0),
+      theme: 'gitlab-theme',
+      format: 'yyyy-mm-dd',
+      onSelect: (dateText) => {
+        const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
+
+        $dueDateInput.val(formattedDate);
+
+        if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+          gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
+          this.updateIssueBoardIssue();
+        } else {
+          this.saveDueDate(true);
+        }
+      }
+    });
+
+    calendar.setDate(new Date($dueDateInput.val()));
+    this.$datePicker.append(calendar.el);
+    this.$datePicker.data('pikaday', calendar);
+  }
+
+  initRemoveDueDate() {
+    this.$block.on('click', '.js-remove-due-date', (e) => {
+      const calendar = this.$datePicker.data('pikaday');
+      e.preventDefault();
+
+      calendar.setDate(null);
+
+      if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+        gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+        this.updateIssueBoardIssue();
+      } else {
+        $("input[name='" + this.fieldName + "']").val('');
+        return this.saveDueDate(false);
+      }
+    });
+  }
+
+  saveDueDate(isDropdown) {
+    this.parseSelectedDate();
+    this.prepSelectedDate();
+    this.submitSelectedDate(isDropdown);
+  }
+
+  parseSelectedDate() {
+    this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
+
+    if (this.rawSelectedDate.length) {
+      // Construct Date object manually to avoid buggy dateString support within Date constructor
+      const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
+      const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
+      this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
+    } else {
+      this.displayedDate = 'No due date';
+    }
+  }
+
+  prepSelectedDate() {
+    const datePayload = {};
+    datePayload[this.abilityName] = {};
+    datePayload[this.abilityName].due_date = this.rawSelectedDate;
+    this.datePayload = datePayload;
+  }
+
+  updateIssueBoardIssue () {
+    this.$loading.fadeIn();
+    this.$dropdown.trigger('loading.gl.dropdown');
+    this.$selectbox.hide();
+    this.$value.css('display', '');
+
+    gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+      .then(() => {
+        this.$loading.fadeOut();
+      });
+  }
+
+  submitSelectedDate(isDropdown) {
+    return $.ajax({
+      type: 'PUT',
+      url: this.issueUpdateURL,
+      data: this.datePayload,
+      dataType: 'json',
+      beforeSend: () => {
+        const selectedDateValue = this.datePayload[this.abilityName].due_date;
+        const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+
+        this.$loading.removeClass('hidden').fadeIn();
+
+        if (isDropdown) {
+          this.$dropdown.trigger('loading.gl.dropdown');
+          this.$selectbox.hide();
+        }
+
+        this.$value.css('display', '');
+        this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
+        this.$sidebarValue.html(this.displayedDate);
+
+        return selectedDateValue.length ?
+          $('.js-remove-due-date-holder').removeClass('hidden') :
+          $('.js-remove-due-date-holder').addClass('hidden');
+      }
+    }).done((data) => {
+      if (isDropdown) {
+        this.$dropdown.trigger('loaded.gl.dropdown');
+        this.$dropdown.dropdown('toggle');
+      }
+      return this.$loading.fadeOut();
+    });
+  }
+}
+
+class DueDateSelectors {
+  constructor() {
+    this.initMilestoneDatePicker();
+    this.initIssuableSelect();
+  }
+
+  initMilestoneDatePicker() {
+    $('.datepicker').each(function() {
+      const $datePicker = $(this);
+      const calendar = new Pikaday({
+        field: $datePicker.get(0),
+        theme: 'gitlab-theme',
+        format: 'yyyy-mm-dd',
+        onSelect(dateText) {
+          $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+        }
+      });
+      calendar.setDate(new Date($datePicker.val()));
+
+      $datePicker.data('pikaday', calendar);
+    });
+
+    $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+      e.preventDefault();
+      const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+      calendar.setDate(null);
+    });
+  }
+
+  initIssuableSelect() {
+    const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+
+    $('.js-due-date-select').each((i, dropdown) => {
+      const $dropdown = $(dropdown);
+      new DueDateSelect({
+        $dropdown,
+        $loading
+      });
+    });
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.DueDateSelectors = DueDateSelectors;
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
deleted file mode 100644
index 9169fcd7328968eca6e9bb43a29e14d298a39318..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/due_date_select.js.es6
+++ /dev/null
@@ -1,204 +0,0 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
-/* global dateFormat */
-/* global Pikaday */
-
-(function(global) {
-  class DueDateSelect {
-    constructor({ $dropdown, $loading } = {}) {
-      const $dropdownParent = $dropdown.closest('.dropdown');
-      const $block = $dropdown.closest('.block');
-      this.$loading = $loading;
-      this.$dropdown = $dropdown;
-      this.$dropdownParent = $dropdownParent;
-      this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
-      this.$block = $block;
-      this.$selectbox = $dropdown.closest('.selectbox');
-      this.$value = $block.find('.value');
-      this.$valueContent = $block.find('.value-content');
-      this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
-      this.fieldName = $dropdown.data('field-name'),
-      this.abilityName = $dropdown.data('ability-name'),
-      this.issueUpdateURL = $dropdown.data('issue-update');
-
-      this.rawSelectedDate = null;
-      this.displayedDate = null;
-      this.datePayload = null;
-
-      this.initGlDropdown();
-      this.initRemoveDueDate();
-      this.initDatePicker();
-    }
-
-    initGlDropdown() {
-      this.$dropdown.glDropdown({
-        opened: () => {
-          const calendar = this.$datePicker.data('pikaday');
-          calendar.show();
-        },
-        hidden: () => {
-          this.$selectbox.hide();
-          this.$value.css('display', '');
-        }
-      });
-    }
-
-    initDatePicker() {
-      const $dueDateInput = $(`input[name='${this.fieldName}']`);
-
-      const calendar = new Pikaday({
-        field: $dueDateInput.get(0),
-        theme: 'gitlab-theme',
-        format: 'yyyy-mm-dd',
-        onSelect: (dateText) => {
-          const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
-
-          $dueDateInput.val(formattedDate);
-
-          if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
-            gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
-            this.updateIssueBoardIssue();
-          } else {
-            this.saveDueDate(true);
-          }
-        }
-      });
-
-      calendar.setDate(new Date($dueDateInput.val()));
-      this.$datePicker.append(calendar.el);
-      this.$datePicker.data('pikaday', calendar);
-    }
-
-    initRemoveDueDate() {
-      this.$block.on('click', '.js-remove-due-date', (e) => {
-        const calendar = this.$datePicker.data('pikaday');
-        e.preventDefault();
-
-        calendar.setDate(null);
-
-        if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
-          gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
-          this.updateIssueBoardIssue();
-        } else {
-          $("input[name='" + this.fieldName + "']").val('');
-          return this.saveDueDate(false);
-        }
-      });
-    }
-
-    saveDueDate(isDropdown) {
-      this.parseSelectedDate();
-      this.prepSelectedDate();
-      this.submitSelectedDate(isDropdown);
-    }
-
-    parseSelectedDate() {
-      this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
-
-      if (this.rawSelectedDate.length) {
-        // Construct Date object manually to avoid buggy dateString support within Date constructor
-        const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
-        const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
-        this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
-      } else {
-        this.displayedDate = 'No due date';
-      }
-    }
-
-    prepSelectedDate() {
-      const datePayload = {};
-      datePayload[this.abilityName] = {};
-      datePayload[this.abilityName].due_date = this.rawSelectedDate;
-      this.datePayload = datePayload;
-    }
-
-    updateIssueBoardIssue () {
-      this.$loading.fadeIn();
-      this.$dropdown.trigger('loading.gl.dropdown');
-      this.$selectbox.hide();
-      this.$value.css('display', '');
-
-      gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
-        .then(() => {
-          this.$loading.fadeOut();
-        });
-    }
-
-    submitSelectedDate(isDropdown) {
-      return $.ajax({
-        type: 'PUT',
-        url: this.issueUpdateURL,
-        data: this.datePayload,
-        dataType: 'json',
-        beforeSend: () => {
-          const selectedDateValue = this.datePayload[this.abilityName].due_date;
-          const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
-
-          this.$loading.fadeIn();
-
-          if (isDropdown) {
-            this.$dropdown.trigger('loading.gl.dropdown');
-            this.$selectbox.hide();
-          }
-
-          this.$value.css('display', '');
-          this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
-          this.$sidebarValue.html(this.displayedDate);
-
-          return selectedDateValue.length ?
-            $('.js-remove-due-date-holder').removeClass('hidden') :
-            $('.js-remove-due-date-holder').addClass('hidden');
-        }
-      }).done((data) => {
-        if (isDropdown) {
-          this.$dropdown.trigger('loaded.gl.dropdown');
-          this.$dropdown.dropdown('toggle');
-        }
-        return this.$loading.fadeOut();
-      });
-    }
-  }
-
-  class DueDateSelectors {
-    constructor() {
-      this.initMilestoneDatePicker();
-      this.initIssuableSelect();
-    }
-
-    initMilestoneDatePicker() {
-      $('.datepicker').each(function() {
-        const $datePicker = $(this);
-        const calendar = new Pikaday({
-          field: $datePicker.get(0),
-          theme: 'gitlab-theme',
-          format: 'yyyy-mm-dd',
-          onSelect(dateText) {
-            $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
-          }
-        });
-        calendar.setDate(new Date($datePicker.val()));
-
-        $datePicker.data('pikaday', calendar);
-      });
-
-      $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
-        e.preventDefault();
-        const calendar = $(e.target).siblings('.datepicker').data('pikaday');
-        calendar.setDate(null);
-      });
-    }
-
-    initIssuableSelect() {
-      const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
-
-      $('.js-due-date-select').each((i, dropdown) => {
-        const $dropdown = $(dropdown);
-        new DueDateSelect({
-          $dropdown,
-          $loading
-        });
-      });
-    }
-  }
-
-  global.DueDateSelectors = DueDateSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js
similarity index 65%
rename from app/assets/javascripts/environments/components/environment.js.es6
rename to app/assets/javascripts/environments/components/environment.js
index 4b700a39d444bdde28b6c7f7beaeecb1fd9020fe..51aab8460f6e16dd6a470bb3c1620e6088446564 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,20 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
 /* global Flash */
+import Vue from 'vue';
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from './environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import eventHub from '../event_hub';
 
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('./environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
-module.exports = Vue.component('environment-component', {
+export default Vue.component('environment-component', {
 
   components: {
     'environment-table': EnvironmentTable,
-    'table-pagination': gl.VueGlPagination,
+    'table-pagination': TablePaginationComponent,
   },
 
   data() {
@@ -35,9 +33,6 @@ module.exports = Vue.component('environment-component', {
       projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
       newEnvironmentPath: environmentsData.newEnvironmentPath,
       helpPagePath: environmentsData.helpPagePath,
-      commitIconSvg: environmentsData.commitIconSvg,
-      playIconSvg: environmentsData.playIconSvg,
-      terminalIconSvg: environmentsData.terminalIconSvg,
 
       // Pagination Properties,
       paginationInformation: {},
@@ -61,7 +56,6 @@ module.exports = Vue.component('environment-component', {
     canCreateEnvironmentParsed() {
       return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
     },
-
   },
 
   /**
@@ -69,33 +63,15 @@ module.exports = Vue.component('environment-component', {
    * Toggles loading property.
    */
   created() {
-    const scope = gl.utils.getParameterByName('scope') || this.visibility;
-    const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
-    const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
-    const service = new EnvironmentsService(endpoint);
-
-    this.isLoading = true;
-
-    return service.all()
-      .then(resp => ({
-        headers: resp.headers,
-        body: resp.json(),
-      }))
-      .then((response) => {
-        this.store.storeAvailableCount(response.body.available_count);
-        this.store.storeStoppedCount(response.body.stopped_count);
-        this.store.storeEnvironments(response.body.environments);
-        this.store.setPagination(response.headers);
-      })
-      .then(() => {
-        this.isLoading = false;
-      })
-      .catch(() => {
-        this.isLoading = false;
-        new Flash('An error occurred while fetching the environments.', 'alert');
-      });
+    this.service = new EnvironmentsService(this.endpoint);
+
+    this.fetchEnvironments();
+
+    eventHub.$on('refreshEnvironments', this.fetchEnvironments);
+  },
+
+  beforeDestroyed() {
+    eventHub.$off('refreshEnvironments');
   },
 
   methods: {
@@ -115,6 +91,32 @@ module.exports = Vue.component('environment-component', {
       gl.utils.visitUrl(param);
       return param;
     },
+
+    fetchEnvironments() {
+      const scope = gl.utils.getParameterByName('scope') || this.visibility;
+      const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+      this.isLoading = true;
+
+      return this.service.get(scope, pageNumber)
+        .then(resp => ({
+          headers: resp.headers,
+          body: resp.json(),
+        }))
+        .then((response) => {
+          this.store.storeAvailableCount(response.body.available_count);
+          this.store.storeStoppedCount(response.body.stopped_count);
+          this.store.storeEnvironments(response.body.environments);
+          this.store.setPagination(response.headers);
+        })
+        .then(() => {
+          this.isLoading = false;
+        })
+        .catch(() => {
+          this.isLoading = false;
+          new Flash('An error occurred while fetching the environments.');
+        });
+    },
   },
 
   template: `
@@ -145,9 +147,9 @@ module.exports = Vue.component('environment-component', {
         </div>
       </div>
 
-      <div class="environments-container">
+      <div class="content-list environments-container">
         <div class="environments-list-loading text-center" v-if="isLoading">
-          <i class="fa fa-spinner fa-spin"></i>
+          <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
         </div>
 
         <div class="blank-state blank-state-no-icon"
@@ -177,16 +179,13 @@ module.exports = Vue.component('environment-component', {
             :environments="state.environments"
             :can-create-deployment="canCreateDeploymentParsed"
             :can-read-environment="canReadEnvironmentParsed"
-            :play-icon-svg="playIconSvg"
-            :terminal-icon-svg="terminalIconSvg"
-            :commit-icon-svg="commitIconSvg">
-          </environment-table>
-
-          <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
-            :change="changePage"
-            :pageInfo="state.paginationInformation">
-          </table-pagination>
+            :service="service"/>
         </div>
+
+        <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+          :change="changePage"
+          :pageInfo="state.paginationInformation">
+        </table-pagination>
       </div>
     </div>
   `,
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..455a88195496e320e43f1fe38eea2db79b06e361
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -0,0 +1,71 @@
+/* global Flash */
+/* eslint-disable no-new */
+
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    actions: {
+      type: Array,
+      required: false,
+      default: () => [],
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      playIconSvg,
+      isLoading: false,
+    };
+  },
+
+  methods: {
+    onClickAction(endpoint) {
+      this.isLoading = true;
+
+      this.service.postAction(endpoint)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshEnvironments');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <div class="btn-group" role="group">
+      <button
+        class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+        data-toggle="dropdown"
+        :disabled="isLoading">
+        <span>
+          <span v-html="playIconSvg"></span>
+          <i class="fa fa-caret-down" aria-hidden="true"></i>
+          <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+        </span>
+
+      <ul class="dropdown-menu dropdown-menu-align-right">
+        <li v-for="action in actions">
+          <button
+            @click="onClickAction(action.play_path)"
+            class="js-manual-action-link no-btn">
+            ${playIconSvg}
+            <span>
+              {{action.name}}
+            </span>
+          </button>
+        </li>
+      </ul>
+    </button>
+  </div>
+  `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
deleted file mode 100644
index c5a714d967337f5de4361cc198a171736a21e30c..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ /dev/null
@@ -1,43 +0,0 @@
-const Vue = require('vue');
-
-module.exports = Vue.component('actions-component', {
-  props: {
-    actions: {
-      type: Array,
-      required: false,
-      default: () => [],
-    },
-
-    playIconSvg: {
-      type: String,
-      required: false,
-    },
-  },
-
-  template: `
-    <div class="inline">
-      <div class="dropdown">
-        <a class="dropdown-new btn btn-default" data-toggle="dropdown">
-          <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
-          <i class="fa fa-caret-down"></i>
-        </a>
-
-        <ul class="dropdown-menu dropdown-menu-align-right">
-          <li v-for="action in actions">
-            <a :href="action.play_path"
-              data-method="post"
-              rel="nofollow"
-              class="js-manual-action-link">
-
-              <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
-
-              <span>
-                {{action.name}}
-              </span>
-            </a>
-          </li>
-        </ul>
-      </div>
-    </div>
-  `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4f9eb357fde7fc82bb96ae057cdc9ccaa88ec97
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -0,0 +1,22 @@
+/**
+ * Renders the external url link in environments table.
+ */
+export default {
+  props: {
+    externalUrl: {
+      type: String,
+      default: '',
+    },
+  },
+
+  template: `
+    <a
+      class="btn external_url"
+      :href="externalUrl"
+      target="_blank"
+      rel="noopener noreferrer"
+      title="Environment external URL">
+      <i class="fa fa-external-link" aria-hidden="true"></i>
+    </a>
+  `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
deleted file mode 100644
index 2599bba3c59211600648038da82a36988e310091..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Renders the external url link in environments table.
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('external-url-component', {
-  props: {
-    externalUrl: {
-      type: String,
-      default: '',
-    },
-  },
-
-  template: `
-    <a class="btn external_url" :href="externalUrl" target="_blank">
-      <i class="fa fa-external-link"></i>
-    </a>
-  `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js
similarity index 84%
rename from app/assets/javascripts/environments/components/environment_item.js.es6
rename to app/assets/javascripts/environments/components/environment_item.js
index 24fd58a301ae7c610efa6ed2dc90562627c652b1..66ed10e19d12f9c80b50a17b70323f5e92bc47cf 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,26 +1,22 @@
-const Vue = require('vue');
-const Timeago = require('timeago.js');
-
-require('../../lib/utils/text_utility');
-require('../../vue_shared/components/commit');
-const ActionsComponent = require('./environment_actions');
-const ExternalUrlComponent = require('./environment_external_url');
-const StopComponent = require('./environment_stop');
-const RollbackComponent = require('./environment_rollback');
-const TerminalButtonComponent = require('./environment_terminal_button');
+import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
+import ActionsComponent from './environment_actions';
+import ExternalUrlComponent from './environment_external_url';
+import StopComponent from './environment_stop';
+import RollbackComponent from './environment_rollback';
+import TerminalButtonComponent from './environment_terminal_button';
+import CommitComponent from '../../vue_shared/components/commit';
 
 /**
  * Envrionment Item Component
  *
  * Renders a table row for each environment.
  */
-
 const timeagoInstance = new Timeago();
 
-module.exports = Vue.component('environment-item', {
-
+export default {
   components: {
-    'commit-component': gl.CommitComponent,
+    'commit-component': CommitComponent,
     'actions-component': ActionsComponent,
     'external-url-component': ExternalUrlComponent,
     'stop-component': StopComponent,
@@ -47,19 +43,9 @@ module.exports = Vue.component('environment-item', {
       default: false,
     },
 
-    commitIconSvg: {
-      type: String,
-      required: false,
-    },
-
-    playIconSvg: {
-      type: String,
-      required: false,
-    },
-
-    terminalIconSvg: {
-      type: String,
-      required: false,
+    service: {
+      type: Object,
+      required: true,
     },
   },
 
@@ -487,9 +473,7 @@ module.exports = Vue.component('environment-item', {
             :commit-url="commitUrl"
             :short-sha="commitShortSha"
             :title="commitTitle"
-            :author="commitAuthor"
-            :commit-icon-svg="commitIconSvg">
-          </commit-component>
+            :author="commitAuthor"/>
         </div>
         <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
           No deployments yet
@@ -503,47 +487,28 @@ module.exports = Vue.component('environment-item', {
         </span>
       </td>
 
-      <td class="hidden-xs">
-        <div v-if="!model.isFolder">
-          <div v-if="hasManualActions && canCreateDeployment"
-            class="inline js-manual-actions-container">
-            <actions-component
-              :play-icon-svg="playIconSvg"
-              :actions="manualActions">
-            </actions-component>
-          </div>
-
-          <div v-if="externalURL && canReadEnvironment"
-            class="inline js-external-url-container">
-            <external-url-component
-              :external-url="externalURL">
-            </external-url-component>
-          </div>
-
-          <div v-if="hasStopAction && canCreateDeployment"
-            class="inline js-stop-component-container">
-            <stop-component
-              :stop-url="model.stop_path">
-            </stop-component>
-          </div>
-
-          <div v-if="model && model.terminal_path"
-            class="inline js-terminal-button-container">
-            <terminal-button-component
-              :terminal-icon-svg="terminalIconSvg"
-              :terminal-path="model.terminal_path">
-            </terminal-button-component>
-          </div>
-
-          <div v-if="canRetry && canCreateDeployment"
-            class="inline js-rollback-component-container">
-            <rollback-component
-              :is-last-deployment="isLastDeployment"
-              :retry-url="retryUrl">
-              </rollback-component>
-          </div>
+      <td class="environments-actions">
+        <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
+          <actions-component v-if="hasManualActions && canCreateDeployment"
+            :service="service"
+            :actions="manualActions"/>
+
+          <external-url-component v-if="externalURL && canReadEnvironment"
+            :external-url="externalURL"/>
+
+          <stop-component v-if="hasStopAction && canCreateDeployment"
+            :stop-url="model.stop_path"
+            :service="service"/>
+
+          <terminal-button-component v-if="model && model.terminal_path"
+            :terminal-path="model.terminal_path"/>
+
+          <rollback-component v-if="canRetry && canCreateDeployment"
+            :is-last-deployment="isLastDeployment"
+            :retry-url="retryUrl"
+            :service="service"/>
         </div>
       </td>
     </tr>
   `,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js
new file mode 100644
index 0000000000000000000000000000000000000000..baa15d9e5b5689f09907d968505ffe8d94957d55
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_rollback.js
@@ -0,0 +1,67 @@
+/* global Flash */
+/* eslint-disable no-new */
+/**
+ * Renders Rollback or Re deploy button in environments table depending
+ * of the provided property `isLastDeployment`.
+ *
+ * Makes a post request when the button is clicked.
+ */
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    retryUrl: {
+      type: String,
+      default: '',
+    },
+
+    isLastDeployment: {
+      type: Boolean,
+      default: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      isLoading: false,
+    };
+  },
+
+  methods: {
+    onClick() {
+      this.isLoading = true;
+
+      this.service.postAction(this.retryUrl)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshEnvironments');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <button type="button"
+      class="btn"
+      @click="onClick"
+      :disabled="isLoading">
+
+      <span v-if="isLastDeployment">
+        Re-deploy
+      </span>
+      <span v-else>
+        Rollback
+      </span>
+
+      <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+    </button>
+  `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
deleted file mode 100644
index daf126eb4e81851033cb734243eee614af6f2c84..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Renders Rollback or Re deploy button in environments table depending
- * of the provided property `isLastDeployment`
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('rollback-component', {
-  props: {
-    retryUrl: {
-      type: String,
-      default: '',
-    },
-
-    isLastDeployment: {
-      type: Boolean,
-      default: true,
-    },
-  },
-
-  template: `
-    <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
-      <span v-if="isLastDeployment">
-        Re-deploy
-      </span>
-      <span v-else>
-        Rollback
-      </span>
-    </a>
-  `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
new file mode 100644
index 0000000000000000000000000000000000000000..5404d6477452d9443b1d06e7fab6fa3c23ff8bfa
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -0,0 +1,56 @@
+/* global Flash */
+/* eslint-disable no-new, no-alert */
+/**
+ * Renders the stop "button" that allows stop an environment.
+ * Used in environments table.
+ */
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    stopUrl: {
+      type: String,
+      default: '',
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      isLoading: false,
+    };
+  },
+
+  methods: {
+    onClick() {
+      if (confirm('Are you sure you want to stop this environment?')) {
+        this.isLoading = true;
+
+        this.service.postAction(this.retryUrl)
+        .then(() => {
+          this.isLoading = false;
+          eventHub.$emit('refreshEnvironments');
+        })
+        .catch(() => {
+          this.isLoading = false;
+          new Flash('An error occured while making the request.', 'alert');
+        });
+      }
+    },
+  },
+
+  template: `
+    <button type="button"
+      class="btn stop-env-link"
+      @click="onClick"
+      :disabled="isLoading"
+      title="Stop Environment">
+      <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
+      <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+    </button>
+  `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
deleted file mode 100644
index 96983a19568aaf943a537eab4f9530a0ec1e35a7..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Renders the stop "button" that allows stop an environment.
- * Used in environments table.
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('stop-component', {
-  props: {
-    stopUrl: {
-      type: String,
-      default: '',
-    },
-  },
-
-  template: `
-    <a class="btn stop-env-link"
-      :href="stopUrl"
-      data-confirm="Are you sure you want to stop this environment?"
-      data-method="post"
-      rel="nofollow">
-      <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
-    </a>
-  `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js
similarity index 52%
rename from app/assets/javascripts/environments/components/environment_terminal_button.js.es6
rename to app/assets/javascripts/environments/components/environment_terminal_button.js
index 481e0d15e7a9ea58407a759d66291a950d729545..66a71faa02f1eaf3dd5b7419e4a61464fe1bc078 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -2,24 +2,26 @@
  * Renders a terminal button to open a web terminal.
  * Used in environments table.
  */
-const Vue = require('vue');
+import terminalIconSvg from 'icons/_icon_terminal.svg';
 
-module.exports = Vue.component('terminal-button-component', {
+export default {
   props: {
     terminalPath: {
       type: String,
+      required: false,
       default: '',
     },
-    terminalIconSvg: {
-      type: String,
-      default: '',
-    },
+  },
+
+  data() {
+    return { terminalIconSvg };
   },
 
   template: `
     <a class="btn terminal-button"
+      title="Open web terminal"
       :href="terminalPath">
-      <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
+      ${terminalIconSvg}
     </a>
   `,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js
similarity index 61%
rename from app/assets/javascripts/environments/components/environments_table.js.es6
rename to app/assets/javascripts/environments/components/environments_table.js
index fd35d77fd3d913b4080fcbbb232de3d67a761763..338dff40bc986982e5604d53435ac37bbf4e409d 100644
--- a/app/assets/javascripts/environments/components/environments_table.js.es6
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,13 +1,11 @@
 /**
  * Render environments table.
  */
-const Vue = require('vue');
-const EnvironmentItem = require('./environment_item');
-
-module.exports = Vue.component('environment-table-component', {
+import EnvironmentTableRowComponent from './environment_item';
 
+export default {
   components: {
-    'environment-item': EnvironmentItem,
+    'environment-item': EnvironmentTableRowComponent,
   },
 
   props: {
@@ -29,24 +27,14 @@ module.exports = Vue.component('environment-table-component', {
       default: false,
     },
 
-    commitIconSvg: {
-      type: String,
-      required: false,
-    },
-
-    playIconSvg: {
-      type: String,
-      required: false,
-    },
-
-    terminalIconSvg: {
-      type: String,
-      required: false,
+    service: {
+      type: Object,
+      required: true,
     },
   },
 
   template: `
-    <table class="table ci-table environments">
+    <table class="table ci-table">
       <thead>
         <tr>
           <th class="environments-name">Environment</th>
@@ -54,7 +42,7 @@ module.exports = Vue.component('environment-table-component', {
           <th class="environments-build">Job</th>
           <th class="environments-commit">Commit</th>
           <th class="environments-date">Updated</th>
-          <th class="hidden-xs environments-actions"></th>
+          <th class="environments-actions"></th>
         </tr>
       </thead>
       <tbody>
@@ -64,11 +52,9 @@ module.exports = Vue.component('environment-table-component', {
             :model="model"
             :can-create-deployment="canCreateDeployment"
             :can-read-environment="canReadEnvironment"
-            :play-icon-svg="playIconSvg"
-            :terminal-icon-svg="terminalIconSvg"
-            :commit-icon-svg="commitIconSvg"></tr>
+            :service="service"></tr>
         </template>
       </tbody>
     </table>
   `,
-});
+};
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js
similarity index 78%
rename from app/assets/javascripts/environments/environments_bundle.js.es6
rename to app/assets/javascripts/environments/environments_bundle.js
index 7bbba91bc109bce83de80b7430c6bbb3aa81b509..8d963b335cfd51116136bfa406e07a69168714d2 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,4 +1,4 @@
-const EnvironmentsComponent = require('./components/environment');
+import EnvironmentsComponent from './components/environment';
 
 $(() => {
   window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/environments/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
similarity index 78%
rename from app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
rename to app/assets/javascripts/environments/folder/environments_folder_bundle.js
index d2ca465351a6887e82e0da5c82250b93cbcb1595..f939eccf2469f5072277491a70a5990f914f4ea1 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,4 +1,4 @@
-const EnvironmentsFolderComponent = require('./environments_folder_view');
+import EnvironmentsFolderComponent from './environments_folder_view';
 
 $(() => {
   window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js
similarity index 84%
rename from app/assets/javascripts/environments/folder/environments_folder_view.js.es6
rename to app/assets/javascripts/environments/folder/environments_folder_view.js
index 53d5296575869aff7878226a315e8c9116a73a65..8abbcf0c227f20ecf4b1b2929bda18ddcee705ea 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
 /* global Flash */
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('../components/environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
-module.exports = Vue.component('environment-folder-view', {
-
+import Vue from 'vue';
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from '../components/environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
+
+export default Vue.component('environment-folder-view', {
   components: {
     'environment-table': EnvironmentTable,
-    'table-pagination': gl.VueGlPagination,
+    'table-pagination': TablePaginationComponent,
   },
 
   data() {
@@ -88,11 +85,11 @@ module.exports = Vue.component('environment-folder-view', {
 
     const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
 
-    const service = new EnvironmentsService(endpoint);
+    this.service = new EnvironmentsService(endpoint);
 
     this.isLoading = true;
 
-    return service.all()
+    return this.service.get()
       .then(resp => ({
         headers: resp.headers,
         body: resp.json(),
@@ -168,13 +165,12 @@ module.exports = Vue.component('environment-folder-view', {
             :can-read-environment="canReadEnvironmentParsed"
             :play-icon-svg="playIconSvg"
             :terminal-icon-svg="terminalIconSvg"
-            :commit-icon-svg="commitIconSvg">
-          </environment-table>
+            :commit-icon-svg="commitIconSvg"
+            :service="service"/>
 
           <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
             :change="changePage"
-            :pageInfo="state.paginationInformation">
-          </table-pagination>
+            :pageInfo="state.paginationInformation"/>
         </div>
       </div>
     </div>
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
new file mode 100644
index 0000000000000000000000000000000000000000..07040bf0d7374db826fa6e4fe15b969853745250
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -0,0 +1,19 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class EnvironmentsService {
+  constructor(endpoint) {
+    this.environments = Vue.resource(endpoint);
+  }
+
+  get(scope, page) {
+    return this.environments.get({ scope, page });
+  }
+
+  postAction(endpoint) {
+    return Vue.http.post(endpoint, {}, { emulateJSON: true });
+  }
+}
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
deleted file mode 100644
index 9cef335868ecc5f75b047997ae0cfa08a7b55f94..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-const Vue = require('vue');
-
-class EnvironmentsService {
-  constructor(endpoint) {
-    this.environments = Vue.resource(endpoint);
-  }
-
-  all() {
-    return this.environments.get();
-  }
-}
-
-module.exports = EnvironmentsService;
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js
similarity index 95%
rename from app/assets/javascripts/environments/stores/environments_store.js.es6
rename to app/assets/javascripts/environments/stores/environments_store.js
index 15cd9bde08e8b4e7f4908f4eee51a512923b6a7f..3c3084f3b78961d85056b23b7cece66fd3cc9193 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,11 +1,11 @@
-require('~/lib/utils/common_utils');
+import '~/lib/utils/common_utils';
 /**
  * Environments Store.
  *
  * Stores received environments, count of stopped environments and count of
  * available environments.
  */
-class EnvironmentsStore {
+export default class EnvironmentsStore {
   constructor() {
     this.state = {};
     this.state.environments = [];
@@ -86,5 +86,3 @@ class EnvironmentsStore {
     return count;
   }
 }
-
-module.exports = EnvironmentsStore;
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
new file mode 100644
index 0000000000000000000000000000000000000000..027222f804dfc023f8a38a482b33031d81ee2cb4
--- /dev/null
+++ b/app/assets/javascripts/extensions/array.js
@@ -0,0 +1,11 @@
+// TODO: remove this
+
+// eslint-disable-next-line no-extend-native
+Array.prototype.first = function first() {
+  return this[0];
+};
+
+// eslint-disable-next-line no-extend-native
+Array.prototype.last = function last() {
+  return this[this.length - 1];
+};
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
deleted file mode 100644
index f8256a8d26d42549273d9d481f40a5e1ad2bb7e8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/extensions/array.js.es6
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */
-
-'use strict';
-
-Array.prototype.first = function() {
-  return this[0];
-};
-
-Array.prototype.last = function() {
-  return this[this.length-1];
-};
-
-Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
-  if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
-  if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
-
-  const list = Object(this);
-  const thisArg = args[1];
-  let value = {};
-
-  for (let i = 0; i < list.length; i += 1) {
-    value = list[i];
-    if (predicate.call(thisArg, value, i, list)) return value;
-  }
-
-  return undefined;
-};
diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6
deleted file mode 100644
index abedae4c1c7a669fad71f73180153be3f09dcde8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/extensions/custom_event.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* global CustomEvent */
-/* eslint-disable no-global-assign */
-
-// Custom event support for IE
-CustomEvent = function CustomEvent(event, parameters) {
-  const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
-  const evt = document.createEvent('CustomEvent');
-  evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
-  return evt;
-};
-
-CustomEvent.prototype = window.Event.prototype;
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
deleted file mode 100644
index 90ab79305a7584a7c7c67c9b32510634b3155ab6..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/extensions/element.js.es6
+++ /dev/null
@@ -1,20 +0,0 @@
-/* global Element */
-/* eslint-disable consistent-return, max-len, no-empty, func-names */
-
-Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
-  if (!selectedElement) return;
-  return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
-};
-
-Element.prototype.matches = Element.prototype.matches ||
-  Element.prototype.matchesSelector ||
-  Element.prototype.mozMatchesSelector ||
-  Element.prototype.msMatchesSelector ||
-  Element.prototype.oMatchesSelector ||
-  Element.prototype.webkitMatchesSelector ||
-  function (s) {
-    const matches = (this.document || this.ownerDocument).querySelectorAll(s);
-    let i = matches.length - 1;
-    while (i >= 0 && matches.item(i) !== this) { i -= 1; }
-    return i > -1;
-  };
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
deleted file mode 100644
index 1a489b859e80b25dcb56eed4eb4fb1ef0b574047..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/extensions/jquery.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
-// Disable an element and add the 'disabled' Bootstrap class
-(function() {
-  $.fn.extend({
-    disable: function() {
-      return $(this).attr('disabled', 'disabled').addClass('disabled');
-    }
-  });
-
-  // Enable an element and remove the 'disabled' Bootstrap class
-  $.fn.extend({
-    enable: function() {
-      return $(this).removeAttr('disabled').removeClass('disabled');
-    }
-  });
-}).call(window);
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
deleted file mode 100644
index 70a2d765abd04cfdb9e140a1db86dda483c24e4d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/extensions/object.js.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-/* 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 698870d0ce1affae29b11b86a19f44fa3b41d271..3f041172ff34a0a4ad2a98b89926fdbb99055c4b 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,147 +1,141 @@
 /* eslint-disable func-names, space-before-function-paren, no-var, 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, consistent-return */
 /* global FilesCommentButton */
+/* global notes */
 
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+let $commentButtonTemplate;
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
 
-  this.FilesCommentButton = (function() {
-    var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
+window.FilesCommentButton = (function() {
+  var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
 
-    COMMENT_BUTTON_CLASS = '.add-diff-note';
+  COMMENT_BUTTON_CLASS = '.add-diff-note';
 
-    COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+  LINE_HOLDER_CLASS = '.line_holder';
 
-    LINE_HOLDER_CLASS = '.line_holder';
+  LINE_NUMBER_CLASS = 'diff-line-num';
 
-    LINE_NUMBER_CLASS = 'diff-line-num';
+  LINE_CONTENT_CLASS = 'line_content';
 
-    LINE_CONTENT_CLASS = 'line_content';
+  UNFOLDABLE_LINE_CLASS = 'js-unfold';
 
-    UNFOLDABLE_LINE_CLASS = 'js-unfold';
+  EMPTY_CELL_CLASS = 'empty-cell';
 
-    EMPTY_CELL_CLASS = 'empty-cell';
+  OLD_LINE_CLASS = 'old_line';
 
-    OLD_LINE_CLASS = 'old_line';
+  LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
 
-    LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
+  TEXT_FILE_SELECTOR = '.text-file';
 
-    TEXT_FILE_SELECTOR = '.text-file';
+  function FilesCommentButton(filesContainerElement) {
+    this.render = bind(this.render, this);
+    this.hideButton = bind(this.hideButton, this);
+    this.isParallelView = notes.isParallelView();
+    filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
+      .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
+  }
 
-    DEBOUNCE_TIMEOUT_DURATION = 100;
+  FilesCommentButton.prototype.render = function(e) {
+    var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
+    $currentTarget = $(e.currentTarget);
 
-    function FilesCommentButton(filesContainerElement) {
-      var debounce;
-      this.filesContainerElement = filesContainerElement;
-      this.destroy = bind(this.destroy, this);
-      this.render = bind(this.render, this);
-      this.VIEW_TYPE = $('input#view[type=hidden]').val();
-      debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
-      $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+    if ($currentTarget.hasClass('js-no-comment-btn')) return;
+
+    lineContentElement = this.getLineContent($currentTarget);
+    buttonParentElement = this.getButtonParent($currentTarget);
+
+    if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
+
+    $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
+    buttonParentElement.addClass('is-over')
+      .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
+
+    if ($button.length) {
+      return;
     }
 
-    FilesCommentButton.prototype.render = function(e) {
-      var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
-      $currentTarget = $(e.currentTarget);
-
-      buttonParentElement = this.getButtonParent($currentTarget);
-      if (!this.validateButtonParent(buttonParentElement)) return;
-      lineContentElement = this.getLineContent($currentTarget);
-      if (!this.validateLineContent(lineContentElement)) return;
-
-      textFileElement = this.getTextFileElement($currentTarget);
-      buttonParentElement.append(this.buildButton({
-        noteableType: textFileElement.attr('data-noteable-type'),
-        noteableID: textFileElement.attr('data-noteable-id'),
-        commitID: textFileElement.attr('data-commit-id'),
-        noteType: lineContentElement.attr('data-note-type'),
-        position: lineContentElement.attr('data-position'),
-        lineType: lineContentElement.attr('data-line-type'),
-        discussionID: lineContentElement.attr('data-discussion-id'),
-        lineCode: lineContentElement.attr('data-line-code')
-      }));
-    };
-
-    FilesCommentButton.prototype.destroy = function(e) {
-      if (this.isMovingToSameType(e)) {
-        return;
-      }
-      $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove();
-    };
-
-    FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
-      var initializedButtonTemplate;
-      initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
-        COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
-      });
-      return $(initializedButtonTemplate).attr({
-        'data-noteable-type': buttonAttributes.noteableType,
-        'data-noteable-id': buttonAttributes.noteableID,
-        'data-commit-id': buttonAttributes.commitID,
-        'data-note-type': buttonAttributes.noteType,
-        'data-line-code': buttonAttributes.lineCode,
-        'data-position': buttonAttributes.position,
-        'data-discussion-id': buttonAttributes.discussionID,
-        'data-line-type': buttonAttributes.lineType
-      });
-    };
-
-    FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
-      return $(hoveredElement.closest(TEXT_FILE_SELECTOR));
-    };
-
-    FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
-      if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+    textFileElement = this.getTextFileElement($currentTarget);
+    buttonParentElement.append(this.buildButton({
+      noteableType: textFileElement.attr('data-noteable-type'),
+      noteableID: textFileElement.attr('data-noteable-id'),
+      commitID: textFileElement.attr('data-commit-id'),
+      noteType: lineContentElement.attr('data-note-type'),
+      position: lineContentElement.attr('data-position'),
+      lineType: lineContentElement.attr('data-line-type'),
+      discussionID: lineContentElement.attr('data-discussion-id'),
+      lineCode: lineContentElement.attr('data-line-code')
+    }));
+  };
+
+  FilesCommentButton.prototype.hideButton = function(e) {
+    var $currentTarget = $(e.currentTarget);
+    var buttonParentElement = this.getButtonParent($currentTarget);
+
+    buttonParentElement.removeClass('is-over')
+      .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
+  };
+
+  FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
+    return $commentButtonTemplate.clone().attr({
+      'data-noteable-type': buttonAttributes.noteableType,
+      'data-noteable-id': buttonAttributes.noteableID,
+      'data-commit-id': buttonAttributes.commitID,
+      'data-note-type': buttonAttributes.noteType,
+      'data-line-code': buttonAttributes.lineCode,
+      'data-position': buttonAttributes.position,
+      'data-discussion-id': buttonAttributes.discussionID,
+      'data-line-type': buttonAttributes.lineType
+    });
+  };
+
+  FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
+    return hoveredElement.closest(TEXT_FILE_SELECTOR);
+  };
+
+  FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
+    if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+      return hoveredElement;
+    }
+    if (!this.isParallelView) {
+      return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
+    } else {
+      return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
+    }
+  };
+
+  FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
+    if (!this.isParallelView) {
+      if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
         return hoveredElement;
       }
-      if (this.VIEW_TYPE === 'inline') {
-        return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
-      } else {
-        return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
-      }
-    };
-
-    FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
-      if (this.VIEW_TYPE === 'inline') {
-        if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
-          return hoveredElement;
-        }
-        return hoveredElement.parent().find("." + OLD_LINE_CLASS);
-      } else {
-        if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
-          return hoveredElement;
-        }
-        return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+      return hoveredElement.parent().find("." + OLD_LINE_CLASS);
+    } else {
+      if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
+        return hoveredElement;
       }
-    };
+      return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+    }
+  };
 
-    FilesCommentButton.prototype.isMovingToSameType = function(e) {
-      var newButtonParent;
-      newButtonParent = this.getButtonParent($(e.toElement));
-      if (!newButtonParent) {
-        return false;
-      }
-      return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
-    };
+  FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
+    return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
+  };
 
-    FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
-      return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0;
-    };
+  FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
+    return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+  };
 
-    FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
-      return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
-    };
+  return FilesCommentButton;
+})();
 
-    return FilesCommentButton;
-  })();
+$.fn.filesCommentButton = function() {
+  $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
 
-  $.fn.filesCommentButton = function() {
-    if (!(this && (this.parent().data('can-create-note') != null))) {
-      return;
+  if (!(this && (this.parent().data('can-create-note') != null))) {
+    return;
+  }
+  return this.each(function() {
+    if (!$.data(this, 'filesCommentButton')) {
+      return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
     }
-    return this.each(function() {
-      if (!$.data(this, 'filesCommentButton')) {
-        return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
-      }
-    });
-  };
-}).call(window);
+  });
+};
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
new file mode 100644
index 0000000000000000000000000000000000000000..aaaeb9bddb1bd1a533be17467bb2d912840d4dbb
--- /dev/null
+++ b/app/assets/javascripts/filterable_list.js
@@ -0,0 +1,46 @@
+/**
+ * Makes search request for content when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+
+export default class FilterableList {
+  constructor(form, filter, holder) {
+    this.filterForm = form;
+    this.listFilterElement = filter;
+    this.listHolderElement = holder;
+  }
+
+  initSearch() {
+    this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
+
+    this.listFilterElement.removeEventListener('input', this.debounceFilter);
+    this.listFilterElement.addEventListener('input', this.debounceFilter);
+  }
+
+  filterResults() {
+    const form = this.filterForm;
+    const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
+
+    $(this.listHolderElement).fadeTo(250, 0.5);
+
+    return $.ajax({
+      url: form.getAttribute('action'),
+      data: $(form).serialize(),
+      type: 'GET',
+      dataType: 'json',
+      context: this,
+      complete() {
+        $(this.listHolderElement).fadeTo(250, 1);
+      },
+      success(data) {
+        this.listHolderElement.innerHTML = data.html;
+
+       // Change url so if user reload a page - search results are saved
+        return window.history.replaceState({
+          page: filterUrl,
+
+        }, document.title, filterUrl);
+      },
+    });
+  }
+}
diff --git a/app/assets/javascripts/filtered_search/container.js b/app/assets/javascripts/filtered_search/container.js
new file mode 100644
index 0000000000000000000000000000000000000000..2243c4dd2c5736482d0c5e647b3e47c8d5555f55
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/container.js
@@ -0,0 +1,14 @@
+/* eslint-disable class-methods-use-this */
+let container = document;
+
+class FilteredSearchContainerClass {
+  set container(containerParam) {
+    container = containerParam;
+  }
+
+  get container() {
+    return container;
+  }
+}
+
+export default new FilteredSearchContainerClass();
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js
similarity index 59%
rename from app/assets/javascripts/filtered_search/dropdown_hint.js.es6
rename to app/assets/javascripts/filtered_search/dropdown_hint.js
index 9e92d544bef816ff6c3595efed759b1aa6e96b76..98dcb697af922865666fe7a55d574aa9bb60a301 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -28,7 +28,24 @@ require('./filtered_search_dropdown');
           const tag = selected.querySelector('.js-filter-tag').innerText.trim();
 
           if (tag.length) {
-            gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
+            // Get previous input values in the input field and convert them into visual tokens
+            const previousInputValues = this.input.value.split(' ');
+            const searchTerms = [];
+
+            previousInputValues.forEach((value, index) => {
+              searchTerms.push(value);
+
+              if (index === previousInputValues.length - 1
+                && token.indexOf(value.toLowerCase()) !== -1) {
+                searchTerms.pop();
+              }
+            });
+
+            if (searchTerms.length > 0) {
+              gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+            }
+
+            gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
           }
           this.dismissDropdown();
           this.dispatchInputEvent();
@@ -39,14 +56,16 @@ require('./filtered_search_dropdown');
     renderContent() {
       const dropdownData = [];
 
-      [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
-        const { icon, hint, tag } = dropdownMenu.dataset;
+      [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+        const { icon, hint, tag, type } = dropdownMenu.dataset;
         if (icon && hint && tag) {
-          dropdownData.push({
-            icon: `fa-${icon}`,
-            hint,
-            tag: `&lt;${tag}&gt;`,
-          });
+          dropdownData.push(
+            Object.assign({
+              icon: `fa-${icon}`,
+              hint,
+              tag: `&lt;${tag}&gt;`,
+            }, type && { type }),
+          );
         }
       });
 
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js
similarity index 100%
rename from app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
rename to app/assets/javascripts/filtered_search/dropdown_non_user.js
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js
similarity index 94%
rename from app/assets/javascripts/filtered_search/dropdown_user.js.es6
rename to app/assets/javascripts/filtered_search/dropdown_user.js
index 7e9c6f74aa56d7bc166b4affd74e402a6ae5c3f3..04e2afad02f4783f91ac580cf098c0aa126d2886 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
     getSearchInput() {
       const query = gl.DropdownUtils.getSearchInput(this.input);
       const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
-      let value = lastToken.value || '';
+
+      let value = lastToken || '';
+
+      if (value[0] === '@') {
+        value = value.slice(1);
+      }
 
       // Removes the first character if it is a quotation so that we can search
       // with multiple words
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..432b0c0dfd2e3242d0888ad4789349bce4f2f960
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -0,0 +1,181 @@
+import FilteredSearchContainer from './container';
+
+(() => {
+  class DropdownUtils {
+    static getEscapedText(text) {
+      let escapedText = text;
+      const hasSpace = text.indexOf(' ') !== -1;
+      const hasDoubleQuote = text.indexOf('"') !== -1;
+
+      // Encapsulate value with quotes if it has spaces
+      // Known side effect: values's with both single and double quotes
+      // won't escape properly
+      if (hasSpace) {
+        if (hasDoubleQuote) {
+          escapedText = `'${text}'`;
+        } else {
+          // Encapsulate singleQuotes or if it hasSpace
+          escapedText = `"${text}"`;
+        }
+      }
+
+      return escapedText;
+    }
+
+    static filterWithSymbol(filterSymbol, input, item) {
+      const updatedItem = item;
+      const searchInput = gl.DropdownUtils.getSearchInput(input);
+
+      const title = updatedItem.title.toLowerCase();
+      let value = searchInput.toLowerCase();
+      let symbol = '';
+
+      // Remove the symbol for filter
+      if (value[0] === filterSymbol) {
+        symbol = value[0];
+        value = value.slice(1);
+      }
+
+      // Removes the first character if it is a quotation so that we can search
+      // with multiple words
+      if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+        value = value.slice(1);
+      }
+
+      // Eg. filterSymbol = ~ for labels
+      const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+      const match = title.indexOf(`${symbol}${value}`) !== -1;
+
+      updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+
+      return updatedItem;
+    }
+
+    static filterHint(input, item) {
+      const updatedItem = item;
+      const searchInput = gl.DropdownUtils.getSearchQuery(input);
+      const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+      const lastKey = lastToken.key || lastToken || '';
+      const allowMultiple = item.type === 'array';
+      const itemInExistingTokens = tokens.some(t => t.key === item.hint);
+
+      if (!allowMultiple && itemInExistingTokens) {
+        updatedItem.droplab_hidden = true;
+      } else if (!lastKey || searchInput.split('').last() === ' ') {
+        updatedItem.droplab_hidden = false;
+      } else if (lastKey) {
+        const split = lastKey.split(':');
+        const tokenName = split[0].split(' ').last();
+
+        const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+        updatedItem.droplab_hidden = tokenName ? match : false;
+      }
+
+      return updatedItem;
+    }
+
+    static setDataValueIfSelected(filter, selected) {
+      const dataValue = selected.getAttribute('data-value');
+
+      if (dataValue) {
+        gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
+      }
+
+      // Return boolean based on whether it was set
+      return dataValue !== null;
+    }
+
+    // Determines the full search query (visual tokens + input)
+    static getSearchQuery(untilInput = false) {
+      const container = FilteredSearchContainer.container;
+      const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
+      const values = [];
+
+      if (untilInput) {
+        const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
+        // Add one to include input-token to the tokens array
+        tokens.splice(inputIndex + 1);
+      }
+
+      tokens.forEach((token) => {
+        if (token.classList.contains('js-visual-token')) {
+          const name = token.querySelector('.name');
+          const value = token.querySelector('.value');
+          const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
+          let valueText = '';
+
+          if (value && value.innerText) {
+            valueText = value.innerText;
+          }
+
+          if (token.className.indexOf('filtered-search-token') !== -1) {
+            values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
+          } else {
+            values.push(name.innerText);
+          }
+        } else if (token.classList.contains('input-token')) {
+          const { isLastVisualTokenValid } =
+            gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+          const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+          const inputValue = input && input.value;
+
+          if (isLastVisualTokenValid) {
+            values.push(inputValue);
+          } else {
+            const previous = values.pop();
+            values.push(`${previous}${inputValue}`);
+          }
+        }
+      });
+
+      return values.join(' ');
+    }
+
+    static getSearchInput(filteredSearchInput) {
+      const inputValue = filteredSearchInput.value;
+      const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+
+      return inputValue.slice(0, right);
+    }
+
+    static getInputSelectionPosition(input) {
+      const selectionStart = input.selectionStart;
+      let inputValue = input.value;
+      // Replace all spaces inside quote marks with underscores
+      // (will continue to match entire string until an end quote is found if any)
+      // This helps with matching the beginning & end of a token:key
+      inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+      // Get the right position for the word selected
+      // Regex matches first space
+      let right = inputValue.slice(selectionStart).search(/\s/);
+
+      if (right >= 0) {
+        right += selectionStart;
+      } else if (right < 0) {
+        right = inputValue.length;
+      }
+
+      // Get the left position for the word selected
+      // Regex matches last non-whitespace character
+      let left = inputValue.slice(0, right).search(/\S+$/);
+
+      if (selectionStart === 0) {
+        left = 0;
+      } else if (selectionStart === inputValue.length && left < 0) {
+        left = inputValue.length;
+      } else if (left < 0) {
+        left = selectionStart;
+      }
+
+      return {
+        left,
+        right,
+      };
+    }
+  }
+
+  window.gl = window.gl || {};
+  gl.DropdownUtils = DropdownUtils;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
deleted file mode 100644
index de3fa1167171baca9a0fc3ab4b5090f103a44826..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ /dev/null
@@ -1,126 +0,0 @@
-(() => {
-  class DropdownUtils {
-    static getEscapedText(text) {
-      let escapedText = text;
-      const hasSpace = text.indexOf(' ') !== -1;
-      const hasDoubleQuote = text.indexOf('"') !== -1;
-
-      // Encapsulate value with quotes if it has spaces
-      // Known side effect: values's with both single and double quotes
-      // won't escape properly
-      if (hasSpace) {
-        if (hasDoubleQuote) {
-          escapedText = `'${text}'`;
-        } else {
-          // Encapsulate singleQuotes or if it hasSpace
-          escapedText = `"${text}"`;
-        }
-      }
-
-      return escapedText;
-    }
-
-    static filterWithSymbol(filterSymbol, input, item) {
-      const updatedItem = item;
-      const query = gl.DropdownUtils.getSearchInput(input);
-      const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
-
-      if (lastToken !== searchToken) {
-        const title = updatedItem.title.toLowerCase();
-        let value = lastToken.value.toLowerCase();
-
-        // Removes the first character if it is a quotation so that we can search
-        // with multiple words
-        if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
-          value = value.slice(1);
-        }
-
-        // Eg. filterSymbol = ~ for labels
-        const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
-        const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
-
-        updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
-      } else {
-        updatedItem.droplab_hidden = false;
-      }
-
-      return updatedItem;
-    }
-
-    static filterHint(input, item) {
-      const updatedItem = item;
-      const query = gl.DropdownUtils.getSearchInput(input);
-      let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
-      lastToken = lastToken.key || lastToken || '';
-
-      if (!lastToken || query.split('').last() === ' ') {
-        updatedItem.droplab_hidden = false;
-      } else if (lastToken) {
-        const split = lastToken.split(':');
-        const tokenName = split[0].split(' ').last();
-
-        const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
-        updatedItem.droplab_hidden = tokenName ? match : false;
-      }
-
-      return updatedItem;
-    }
-
-    static setDataValueIfSelected(filter, selected) {
-      const dataValue = selected.getAttribute('data-value');
-
-      if (dataValue) {
-        gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
-      }
-
-      // Return boolean based on whether it was set
-      return dataValue !== null;
-    }
-
-    static getSearchInput(filteredSearchInput) {
-      const inputValue = filteredSearchInput.value;
-      const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
-
-      return inputValue.slice(0, right);
-    }
-
-    static getInputSelectionPosition(input) {
-      const selectionStart = input.selectionStart;
-      let inputValue = input.value;
-      // Replace all spaces inside quote marks with underscores
-      // (will continue to match entire string until an end quote is found if any)
-      // This helps with matching the beginning & end of a token:key
-      inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
-
-      // Get the right position for the word selected
-      // Regex matches first space
-      let right = inputValue.slice(selectionStart).search(/\s/);
-
-      if (right >= 0) {
-        right += selectionStart;
-      } else if (right < 0) {
-        right = inputValue.length;
-      }
-
-      // Get the left position for the word selected
-      // Regex matches last non-whitespace character
-      let left = inputValue.slice(0, right).search(/\S+$/);
-
-      if (selectionStart === 0) {
-        left = 0;
-      } else if (selectionStart === inputValue.length && left < 0) {
-        left = inputValue.length;
-      } else if (left < 0) {
-        left = selectionStart;
-      }
-
-      return {
-        left,
-        right,
-      };
-    }
-  }
-
-  window.gl = window.gl || {};
-  gl.DropdownUtils = DropdownUtils;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index 392f18359663fa5edf1bbb4950552435ba78b263..856eb6590ee67c3ad3a0062bded4e4f4b3bb5c64 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,3 +1,10 @@
-function requireAll(context) { return context.keys().map(context); }
-
-requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
+require('./dropdown_hint');
+require('./dropdown_non_user');
+require('./dropdown_user');
+require('./dropdown_utils');
+require('./filtered_search_dropdown_manager');
+require('./filtered_search_dropdown');
+require('./filtered_search_manager');
+require('./filtered_search_token_keys');
+require('./filtered_search_tokenizer');
+require('./filtered_search_visual_tokens');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
similarity index 92%
rename from app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
rename to app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index fbc72a3001a0aa125866bf5d144dc21b6b63da31..e7bf530d3430cd63f0937e9bd4fd7faca24f74f2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -35,9 +35,10 @@
 
         if (!dataValueSet) {
           const value = getValueFunction(selected);
-          gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+          gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
         }
 
+        this.resetFilters();
         this.dismissDropdown();
         this.dispatchInputEvent();
       }
@@ -48,7 +49,11 @@
     }
 
     setOffset(offset = 0) {
-      this.dropdown.style.left = `${offset}px`;
+      if (window.innerWidth > 480) {
+        this.dropdown.style.left = `${offset}px`;
+      } else {
+        this.dropdown.style.left = '0px';
+      }
     }
 
     renderContent(forceShowList = false) {
@@ -103,7 +108,7 @@
       const hook = this.getCurrentHook();
 
       if (hook) {
-        const data = hook.list.data;
+        const data = hook.list.data || [];
         const results = data.map((o) => {
           const updated = o;
           updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
similarity index 67%
rename from app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
rename to app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cecd3518ce3f5d89514f458f6c594ab728b0a007..5fbe0450bb88d8f2a7b437448114389c0d3b8f3d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,12 +1,14 @@
 /* global DropLab */
+import FilteredSearchContainer from './container';
 
 (() => {
   class FilteredSearchDropdownManager {
     constructor(baseEndpoint = '', page) {
+      this.container = FilteredSearchContainer.container;
       this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
       this.tokenizer = gl.FilteredSearchTokenizer;
       this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
-      this.filteredSearchInput = document.querySelector('.filtered-search');
+      this.filteredSearchInput = this.container.querySelector('.filtered-search');
       this.page = page;
 
       this.setupMapping();
@@ -31,62 +33,42 @@
         author: {
           reference: null,
           gl: 'DropdownUser',
-          element: document.querySelector('#js-dropdown-author'),
+          element: this.container.querySelector('#js-dropdown-author'),
         },
         assignee: {
           reference: null,
           gl: 'DropdownUser',
-          element: document.querySelector('#js-dropdown-assignee'),
+          element: this.container.querySelector('#js-dropdown-assignee'),
         },
         milestone: {
           reference: null,
           gl: 'DropdownNonUser',
           extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
-          element: document.querySelector('#js-dropdown-milestone'),
+          element: this.container.querySelector('#js-dropdown-milestone'),
         },
         label: {
           reference: null,
           gl: 'DropdownNonUser',
           extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
-          element: document.querySelector('#js-dropdown-label'),
+          element: this.container.querySelector('#js-dropdown-label'),
         },
         hint: {
           reference: null,
           gl: 'DropdownHint',
-          element: document.querySelector('#js-dropdown-hint'),
+          element: this.container.querySelector('#js-dropdown-hint'),
         },
       };
     }
 
-    static addWordToInput(tokenName, tokenValue = '') {
-      const input = document.querySelector('.filtered-search');
-      const inputValue = input.value;
-      const word = `${tokenName}:${tokenValue}`;
+    static addWordToInput(tokenName, tokenValue = '', clicked = false) {
+      const input = FilteredSearchContainer.container.querySelector('.filtered-search');
 
-      // Get the string to replace
-      let newCaretPosition = input.selectionStart;
-      const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+      gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+      input.value = '';
 
-      input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
-
-      // If we have added a tokenValue at the end of the input,
-      // add a space and set selection to the end
-      if (right >= inputValue.length && tokenValue !== '') {
-        input.value += ' ';
-        newCaretPosition = input.value.length;
+      if (clicked) {
+        gl.FilteredSearchVisualTokens.moveInputToTheRight();
       }
-
-      gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
-    }
-
-    static updateInputCaretPosition(selectionStart, input) {
-      // Reset the position
-      // Sometimes can end up at end of input
-      input.setSelectionRange(selectionStart, selectionStart);
-
-      const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
-
-      input.setSelectionRange(right, right);
     }
 
     updateCurrentDropdownOffset() {
@@ -94,19 +76,14 @@
     }
 
     updateDropdownOffset(key) {
-      if (!this.font) {
-        this.font = window.getComputedStyle(this.filteredSearchInput).font;
-      }
-
-      const input = this.filteredSearchInput;
-      const inputText = input.value.slice(0, input.selectionStart);
-      const filterIconPadding = 27;
-      let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
+      // Always align dropdown with the input field
+      let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
 
-      const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
-      this.mapping[key].element.clientWidth;
-      const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
+      const maxInputWidth = 240;
+      const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
 
+      // Make sure offset never exceeds the input container
+      const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
       if (offsetMaxWidth < offset) {
         offset = offsetMaxWidth;
       }
@@ -164,8 +141,8 @@
     }
 
     setDropdown() {
-      const { lastToken, searchToken } = this.tokenizer
-        .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
+      const query = gl.DropdownUtils.getSearchQuery(true);
+      const { lastToken, searchToken } = this.tokenizer.processTokens(query);
 
       if (this.currentDropdown) {
         this.updateCurrentDropdownOffset();
@@ -187,6 +164,10 @@
     }
 
     resetDropdowns() {
+      if (!this.currentDropdown) {
+        return;
+      }
+
       // Force current dropdown to hide
       this.mapping[this.currentDropdown].reference.hideDropdown();
 
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6bb7fda8f21ab04c8c5ea70c778a6649cd0bd12
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -0,0 +1,423 @@
+import FilteredSearchContainer from './container';
+
+(() => {
+  class FilteredSearchManager {
+    constructor(page) {
+      this.container = FilteredSearchContainer.container;
+      this.filteredSearchInput = this.container.querySelector('.filtered-search');
+      this.clearSearchButton = this.container.querySelector('.clear-search');
+      this.tokensContainer = this.container.querySelector('.tokens-container');
+      this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+
+      if (this.filteredSearchInput) {
+        this.tokenizer = gl.FilteredSearchTokenizer;
+        this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
+
+        this.bindEvents();
+        this.loadSearchParamsFromURL();
+        this.dropdownManager.setDropdown();
+
+        this.cleanupWrapper = this.cleanup.bind(this);
+        document.addEventListener('beforeunload', this.cleanupWrapper);
+      }
+    }
+
+    cleanup() {
+      this.unbindEvents();
+      document.removeEventListener('beforeunload', this.cleanupWrapper);
+    }
+
+    bindEvents() {
+      this.handleFormSubmit = this.handleFormSubmit.bind(this);
+      this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
+      this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+      this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+      this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
+      this.checkForEnterWrapper = this.checkForEnter.bind(this);
+      this.clearSearchWrapper = this.clearSearch.bind(this);
+      this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+      this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+      this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
+      this.editTokenWrapper = this.editToken.bind(this);
+      this.tokenChange = this.tokenChange.bind(this);
+      this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+      this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
+
+      this.filteredSearchInputForm = this.filteredSearchInput.form;
+      this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
+      this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
+      this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+      this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
+      this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
+      this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
+      this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+      this.filteredSearchInput.addEventListener('click', this.tokenChange);
+      this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+      this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
+      this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+      this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
+      this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+      document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+      document.addEventListener('click', this.unselectEditTokensWrapper);
+      document.addEventListener('click', this.removeInputContainerFocusWrapper);
+      document.addEventListener('keydown', this.removeSelectedTokenWrapper);
+    }
+
+    unbindEvents() {
+      this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
+      this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
+      this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+      this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+      this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
+      this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
+      this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+      this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+      this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+      this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
+      this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+      this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
+      this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+      document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+      document.removeEventListener('click', this.unselectEditTokensWrapper);
+      document.removeEventListener('click', this.removeInputContainerFocusWrapper);
+      document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
+    }
+
+    checkForBackspace(e) {
+      // 8 = Backspace Key
+      // 46 = Delete Key
+      if (e.keyCode === 8 || e.keyCode === 46) {
+        const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+        if (this.filteredSearchInput.value === '' && lastVisualToken) {
+          this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+          gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+        }
+
+        // Reposition dropdown so that it is aligned with cursor
+        this.dropdownManager.updateCurrentDropdownOffset();
+      }
+    }
+
+    checkForEnter(e) {
+      if (e.keyCode === 38 || e.keyCode === 40) {
+        const selectionStart = this.filteredSearchInput.selectionStart;
+
+        e.preventDefault();
+        this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+      }
+
+      if (e.keyCode === 13) {
+        const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+        const dropdownEl = dropdown.element;
+        const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
+        e.preventDefault();
+
+        if (!activeElements.length) {
+          if (this.isHandledAsync) {
+            e.stopImmediatePropagation();
+
+            this.filteredSearchInput.blur();
+            this.dropdownManager.resetDropdowns();
+          } else {
+            // Prevent droplab from opening dropdown
+            this.dropdownManager.destroyDroplab();
+          }
+
+          this.search();
+        }
+      }
+    }
+
+    addInputContainerFocus() {
+      const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+      if (inputContainer) {
+        inputContainer.classList.add('focus');
+      }
+    }
+
+    removeInputContainerFocus(e) {
+      const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+      const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+      const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+      const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+      if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+        !isElementInStaticFilterDropdown && inputContainer) {
+        inputContainer.classList.remove('focus');
+      }
+    }
+
+    static selectToken(e) {
+      const button = e.target.closest('.selectable');
+
+      if (button) {
+        e.preventDefault();
+        e.stopPropagation();
+        gl.FilteredSearchVisualTokens.selectToken(button);
+      }
+    }
+
+    unselectEditTokens(e) {
+      const inputContainer = this.container.querySelector('.filtered-search-input-container');
+      const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+      const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+      const isElementTokensContainer = e.target.classList.contains('tokens-container');
+
+      if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+        gl.FilteredSearchVisualTokens.moveInputToTheRight();
+        this.dropdownManager.resetDropdowns();
+      }
+    }
+
+    editToken(e) {
+      const token = e.target.closest('.js-visual-token');
+
+      if (token) {
+        gl.FilteredSearchVisualTokens.editToken(token);
+        this.tokenChange();
+      }
+    }
+
+    toggleClearSearchButton() {
+      const query = gl.DropdownUtils.getSearchQuery();
+      const hidden = 'hidden';
+      const hasHidden = this.clearSearchButton.classList.contains(hidden);
+
+      if (query.length === 0 && !hasHidden) {
+        this.clearSearchButton.classList.add(hidden);
+      } else if (query.length && hasHidden) {
+        this.clearSearchButton.classList.remove(hidden);
+      }
+    }
+
+    handleInputPlaceholder() {
+      const query = gl.DropdownUtils.getSearchQuery();
+      const placeholder = 'Search or filter results...';
+      const currentPlaceholder = this.filteredSearchInput.placeholder;
+
+      if (query.length === 0 && currentPlaceholder !== placeholder) {
+        this.filteredSearchInput.placeholder = placeholder;
+      } else if (query.length > 0 && currentPlaceholder !== '') {
+        this.filteredSearchInput.placeholder = '';
+      }
+    }
+
+    removeSelectedToken(e) {
+      // 8 = Backspace Key
+      // 46 = Delete Key
+      if (e.keyCode === 8 || e.keyCode === 46) {
+        gl.FilteredSearchVisualTokens.removeSelectedToken();
+        this.handleInputPlaceholder();
+        this.toggleClearSearchButton();
+      }
+    }
+
+    clearSearch(e) {
+      e.preventDefault();
+
+      this.filteredSearchInput.value = '';
+
+      const removeElements = [];
+
+      [].forEach.call(this.tokensContainer.children, (t) => {
+        if (t.classList.contains('js-visual-token')) {
+          removeElements.push(t);
+        }
+      });
+
+      removeElements.forEach((el) => {
+        el.parentElement.removeChild(el);
+      });
+
+      this.clearSearchButton.classList.add('hidden');
+      this.handleInputPlaceholder();
+
+      this.dropdownManager.resetDropdowns();
+
+      if (this.isHandledAsync) {
+        this.search();
+      }
+    }
+
+    handleInputVisualToken() {
+      const input = this.filteredSearchInput;
+      const { tokens, searchToken }
+        = gl.FilteredSearchTokenizer.processTokens(input.value);
+      const { isLastVisualTokenValid }
+        = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+      if (isLastVisualTokenValid) {
+        tokens.forEach((t) => {
+          input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+          gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+        });
+
+        const fragments = searchToken.split(':');
+        if (fragments.length > 1) {
+          const inputValues = fragments[0].split(' ');
+          const tokenKey = inputValues.last();
+
+          if (inputValues.length > 1) {
+            inputValues.pop();
+            const searchTerms = inputValues.join(' ');
+
+            input.value = input.value.replace(searchTerms, '');
+            gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
+          }
+
+          gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+          input.value = input.value.replace(`${tokenKey}:`, '');
+        }
+      } else {
+        // Keep listening to token until we determine that the user is done typing the token value
+        const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
+
+        if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+          gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+
+          // Trim the last space as seen in the if statement above
+          input.value = input.value.replace(searchToken, '').trim();
+        }
+      }
+    }
+
+    handleFormSubmit(e) {
+      e.preventDefault();
+      this.search();
+    }
+
+    loadSearchParamsFromURL() {
+      const params = gl.utils.getUrlParamsArray();
+      const usernameParams = this.getUsernameParams();
+      let hasFilteredSearch = false;
+
+      params.forEach((p) => {
+        const split = p.split('=');
+        const keyParam = decodeURIComponent(split[0]);
+        const value = split[1];
+
+        // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
+        const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+
+        if (condition) {
+          hasFilteredSearch = true;
+          gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
+        } else {
+          // Sanitize value since URL converts spaces into +
+          // Replace before decode so that we know what was originally + versus the encoded +
+          const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+          const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+          if (match) {
+            const indexOf = keyParam.indexOf('_');
+            const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+            const symbol = match.symbol;
+            let quotationsToUse = '';
+
+            if (sanitizedValue.indexOf(' ') !== -1) {
+              // Prefer ", but use ' if required
+              quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+            }
+
+            hasFilteredSearch = true;
+            gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+          } else if (!match && keyParam === 'assignee_id') {
+            const id = parseInt(value, 10);
+            if (usernameParams[id]) {
+              hasFilteredSearch = true;
+              gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
+            }
+          } else if (!match && keyParam === 'author_id') {
+            const id = parseInt(value, 10);
+            if (usernameParams[id]) {
+              hasFilteredSearch = true;
+              gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
+            }
+          } else if (!match && keyParam === 'search') {
+            hasFilteredSearch = true;
+            this.filteredSearchInput.value = sanitizedValue;
+          }
+        }
+      });
+
+      if (hasFilteredSearch) {
+        this.clearSearchButton.classList.remove('hidden');
+        this.handleInputPlaceholder();
+      }
+    }
+
+    search() {
+      const paths = [];
+      const { tokens, searchToken }
+        = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery());
+      const currentState = gl.utils.getParameterByName('state') || 'opened';
+      paths.push(`state=${currentState}`);
+
+      tokens.forEach((token) => {
+        const condition = this.filteredSearchTokenKeys
+          .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+        const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+        const keyParam = param ? `${token.key}_${param}` : token.key;
+        let tokenPath = '';
+
+        if (condition) {
+          tokenPath = condition.url;
+        } else {
+          let tokenValue = token.value;
+
+          if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+            (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+            tokenValue = tokenValue.slice(1, tokenValue.length - 1);
+          }
+
+          tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+        }
+
+        paths.push(tokenPath);
+      });
+
+      if (searchToken) {
+        const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+        paths.push(`search=${sanitized}`);
+      }
+
+      const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
+
+      if (this.updateObject) {
+        this.updateObject(parameterizedUrl);
+      } else {
+        gl.utils.visitUrl(parameterizedUrl);
+      }
+    }
+
+    getUsernameParams() {
+      const usernamesById = {};
+      try {
+        const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+        JSON.parse(attribute).forEach((user) => {
+          usernamesById[user.id] = user.username;
+        });
+      } catch (e) {
+        // do nothing
+      }
+      return usernamesById;
+    }
+
+    tokenChange() {
+      const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+
+      if (dropdown) {
+        const currentDropdownRef = dropdown.reference;
+
+        this.setDropdownWrapper();
+        currentDropdownRef.dispatchInputEvent();
+      }
+    }
+  }
+
+  window.gl = window.gl || {};
+  gl.FilteredSearchManager = FilteredSearchManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
deleted file mode 100644
index bbafead03054222a85446326e17b288ab48c7287..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ /dev/null
@@ -1,231 +0,0 @@
-(() => {
-  class FilteredSearchManager {
-    constructor(page) {
-      this.filteredSearchInput = document.querySelector('.filtered-search');
-      this.clearSearchButton = document.querySelector('.clear-search');
-      this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
-
-      if (this.filteredSearchInput) {
-        this.tokenizer = gl.FilteredSearchTokenizer;
-        this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
-
-        this.bindEvents();
-        this.loadSearchParamsFromURL();
-        this.dropdownManager.setDropdown();
-
-        this.cleanupWrapper = this.cleanup.bind(this);
-        document.addEventListener('beforeunload', this.cleanupWrapper);
-      }
-    }
-
-    cleanup() {
-      this.unbindEvents();
-      document.removeEventListener('beforeunload', this.cleanupWrapper);
-    }
-
-    bindEvents() {
-      this.handleFormSubmit = this.handleFormSubmit.bind(this);
-      this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
-      this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
-      this.checkForEnterWrapper = this.checkForEnter.bind(this);
-      this.clearSearchWrapper = this.clearSearch.bind(this);
-      this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
-      this.tokenChange = this.tokenChange.bind(this);
-
-      this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
-      this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
-      this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
-      this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
-      this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
-      this.filteredSearchInput.addEventListener('click', this.tokenChange);
-      this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
-      this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
-    }
-
-    unbindEvents() {
-      this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
-      this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
-      this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
-      this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
-      this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
-      this.filteredSearchInput.removeEventListener('click', this.tokenChange);
-      this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
-      this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
-    }
-
-    checkForBackspace(e) {
-      // 8 = Backspace Key
-      // 46 = Delete Key
-      if (e.keyCode === 8 || e.keyCode === 46) {
-        // Reposition dropdown so that it is aligned with cursor
-        this.dropdownManager.updateCurrentDropdownOffset();
-      }
-    }
-
-    checkForEnter(e) {
-      if (e.keyCode === 38 || e.keyCode === 40) {
-        const selectionStart = this.filteredSearchInput.selectionStart;
-
-        e.preventDefault();
-        this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
-      }
-
-      if (e.keyCode === 13) {
-        const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
-        const dropdownEl = dropdown.element;
-        const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
-
-        e.preventDefault();
-
-        if (!activeElements.length) {
-          // Prevent droplab from opening dropdown
-          this.dropdownManager.destroyDroplab();
-
-          this.search();
-        }
-      }
-    }
-
-    toggleClearSearchButton(e) {
-      if (e.target.value) {
-        this.clearSearchButton.classList.remove('hidden');
-      } else {
-        this.clearSearchButton.classList.add('hidden');
-      }
-    }
-
-    clearSearch(e) {
-      e.preventDefault();
-
-      this.filteredSearchInput.value = '';
-      this.clearSearchButton.classList.add('hidden');
-
-      this.dropdownManager.resetDropdowns();
-    }
-
-    handleFormSubmit(e) {
-      e.preventDefault();
-      this.search();
-    }
-
-    loadSearchParamsFromURL() {
-      const params = gl.utils.getUrlParamsArray();
-      const usernameParams = this.getUsernameParams();
-      const inputValues = [];
-
-      params.forEach((p) => {
-        const split = p.split('=');
-        const keyParam = decodeURIComponent(split[0]);
-        const value = split[1];
-
-        // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
-        const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
-
-        if (condition) {
-          inputValues.push(`${condition.tokenKey}:${condition.value}`);
-        } else {
-          // Sanitize value since URL converts spaces into +
-          // Replace before decode so that we know what was originally + versus the encoded +
-          const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
-          const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
-
-          if (match) {
-            const indexOf = keyParam.indexOf('_');
-            const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
-            const symbol = match.symbol;
-            let quotationsToUse = '';
-
-            if (sanitizedValue.indexOf(' ') !== -1) {
-              // Prefer ", but use ' if required
-              quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
-            }
-
-            inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
-          } else if (!match && keyParam === 'assignee_id') {
-            const id = parseInt(value, 10);
-            if (usernameParams[id]) {
-              inputValues.push(`assignee:@${usernameParams[id]}`);
-            }
-          } else if (!match && keyParam === 'author_id') {
-            const id = parseInt(value, 10);
-            if (usernameParams[id]) {
-              inputValues.push(`author:@${usernameParams[id]}`);
-            }
-          } else if (!match && keyParam === 'search') {
-            inputValues.push(sanitizedValue);
-          }
-        }
-      });
-
-      // Trim the last space value
-      this.filteredSearchInput.value = inputValues.join(' ');
-
-      if (inputValues.length > 0) {
-        this.clearSearchButton.classList.remove('hidden');
-      }
-    }
-
-    search() {
-      const paths = [];
-      const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
-      const currentState = gl.utils.getParameterByName('state') || 'opened';
-      paths.push(`state=${currentState}`);
-
-      tokens.forEach((token) => {
-        const condition = this.filteredSearchTokenKeys
-          .searchByConditionKeyValue(token.key, token.value.toLowerCase());
-        const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
-        const keyParam = param ? `${token.key}_${param}` : token.key;
-        let tokenPath = '';
-
-        if (condition) {
-          tokenPath = condition.url;
-        } else {
-          let tokenValue = token.value;
-
-          if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
-            (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
-            tokenValue = tokenValue.slice(1, tokenValue.length - 1);
-          }
-
-          tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
-        }
-
-        paths.push(tokenPath);
-      });
-
-      if (searchToken) {
-        const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
-        paths.push(`search=${sanitized}`);
-      }
-
-      const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
-
-      gl.utils.visitUrl(parameterizedUrl);
-    }
-
-    getUsernameParams() {
-      const usernamesById = {};
-      try {
-        const attribute = this.filteredSearchInput.getAttribute('data-username-params');
-        JSON.parse(attribute).forEach((user) => {
-          usernamesById[user.id] = user.username;
-        });
-      } catch (e) {
-        // do nothing
-      }
-      return usernamesById;
-    }
-
-    tokenChange() {
-      const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
-      const currentDropdownRef = dropdown.reference;
-
-      this.setDropdownWrapper();
-      currentDropdownRef.dispatchInputEvent();
-    }
-  }
-
-  window.gl = window.gl || {};
-  gl.FilteredSearchManager = FilteredSearchManager;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
similarity index 95%
rename from app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
rename to app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index e6b53cd4b55f66f194bdbcd109134a16fa184bdd..6d5df86f2a551e815d34bb6d028d0d1615735106 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -42,6 +42,10 @@
     url: 'milestone_title=%23upcoming',
     tokenKey: 'milestone',
     value: 'upcoming',
+  }, {
+    url: 'milestone_title=%23started',
+    tokenKey: 'milestone',
+    value: 'started',
   }, {
     url: 'label_name[]=No+Label',
     tokenKey: 'label',
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
similarity index 100%
rename from app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
rename to app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
new file mode 100644
index 0000000000000000000000000000000000000000..a5657fc8720f7f26550bc250900f07d5d28db69d
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -0,0 +1,202 @@
+import FilteredSearchContainer from './container';
+
+class FilteredSearchVisualTokens {
+  static getLastVisualTokenBeforeInput() {
+    const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
+    const lastVisualToken = inputLi && inputLi.previousElementSibling;
+
+    return {
+      lastVisualToken,
+      isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
+    };
+  }
+
+  static unselectTokens() {
+    const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
+    [].forEach.call(otherTokens, t => t.classList.remove('selected'));
+  }
+
+  static selectToken(tokenButton) {
+    const selected = tokenButton.classList.contains('selected');
+    FilteredSearchVisualTokens.unselectTokens();
+
+    if (!selected) {
+      tokenButton.classList.add('selected');
+    }
+  }
+
+  static removeSelectedToken() {
+    const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
+
+    if (selected) {
+      const li = selected.closest('.js-visual-token');
+      li.parentElement.removeChild(li);
+    }
+  }
+
+  static createVisualTokenElementHTML() {
+    return `
+      <div class="selectable" role="button">
+        <div class="name"></div>
+        <div class="value"></div>
+      </div>
+    `;
+  }
+
+  static addVisualTokenElement(name, value, isSearchTerm) {
+    const li = document.createElement('li');
+    li.classList.add('js-visual-token');
+    li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+
+    if (value) {
+      li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+      li.querySelector('.value').innerText = value;
+    } else {
+      li.innerHTML = '<div class="name"></div>';
+    }
+    li.querySelector('.name').innerText = name;
+
+    const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+    const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+    tokensContainer.insertBefore(li, input.parentElement);
+  }
+
+  static addValueToPreviousVisualTokenElement(value) {
+    const { lastVisualToken, isLastVisualTokenValid } =
+      FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+    if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
+      const name = FilteredSearchVisualTokens.getLastTokenPartial();
+      lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+      lastVisualToken.querySelector('.name').innerText = name;
+      lastVisualToken.querySelector('.value').innerText = value;
+    }
+  }
+
+  static addFilterVisualToken(tokenName, tokenValue) {
+    const { lastVisualToken, isLastVisualTokenValid }
+      = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+    const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+
+    if (isLastVisualTokenValid) {
+      addVisualTokenElement(tokenName, tokenValue, false);
+    } else {
+      const previousTokenName = lastVisualToken.querySelector('.name').innerText;
+      const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+      tokensContainer.removeChild(lastVisualToken);
+
+      const value = tokenValue || tokenName;
+      addVisualTokenElement(previousTokenName, value, false);
+    }
+  }
+
+  static addSearchVisualToken(searchTerm) {
+    const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+    if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+      lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
+    } else {
+      FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+    }
+  }
+
+  static getLastTokenPartial() {
+    const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+    if (!lastVisualToken) return '';
+
+    const value = lastVisualToken.querySelector('.value');
+    const name = lastVisualToken.querySelector('.name');
+
+    const valueText = value ? value.innerText : '';
+    const nameText = name ? name.innerText : '';
+
+    return valueText || nameText;
+  }
+
+  static removeLastTokenPartial() {
+    const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+    if (lastVisualToken) {
+      const value = lastVisualToken.querySelector('.value');
+
+      if (value) {
+        const button = lastVisualToken.querySelector('.selectable');
+        button.removeChild(value);
+        lastVisualToken.innerHTML = button.innerHTML;
+      } else {
+        lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
+      }
+    }
+  }
+
+  static tokenizeInput() {
+    const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+    const { isLastVisualTokenValid } =
+      gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+    if (input.value) {
+      if (isLastVisualTokenValid) {
+        gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
+      } else {
+        FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
+      }
+
+      input.value = '';
+    }
+  }
+
+  static editToken(token) {
+    const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+    FilteredSearchVisualTokens.tokenizeInput();
+
+    // Replace token with input field
+    const tokenContainer = token.parentElement;
+    const inputLi = input.parentElement;
+    tokenContainer.replaceChild(inputLi, token);
+
+    const name = token.querySelector('.name');
+    const value = token.querySelector('.value');
+
+    if (token.classList.contains('filtered-search-token') && value) {
+      FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
+      input.value = value.innerText;
+    } else {
+      // token is a search term
+      input.value = name.innerText;
+    }
+
+    // Opens dropdown
+    const inputEvent = new Event('input');
+    input.dispatchEvent(inputEvent);
+
+    // Adds cursor to input
+    input.focus();
+  }
+
+  static moveInputToTheRight() {
+    const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+    const inputLi = input.parentElement;
+    const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+
+    FilteredSearchVisualTokens.tokenizeInput();
+
+    if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
+      const { isLastVisualTokenValid } =
+        gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+      if (!isLastVisualTokenValid) {
+        const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+        gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+        gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
+      }
+
+      tokenContainer.removeChild(inputLi);
+      tokenContainer.appendChild(inputLi);
+    }
+  }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 730104b89f9a5661fa68a9100bc7092b8a73ae26..eec30624ff251c0279ec24d9f90984494d453732 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,42 +1,41 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
-(function() {
-  this.Flash = (function() {
-    var hideFlash;
 
-    hideFlash = function() {
-      return $(this).fadeOut();
-    };
+window.Flash = (function() {
+  var hideFlash;
 
-    function Flash(message, type, parent) {
-      var flash, textDiv;
-      if (type == null) {
-        type = 'alert';
-      }
-      if (parent == null) {
-        parent = null;
-      }
-      if (parent) {
-        this.flashContainer = parent.find('.flash-container');
-      } else {
-        this.flashContainer = $('.flash-container-page');
-      }
-      this.flashContainer.html('');
-      flash = $('<div/>', {
-        "class": "flash-" + type
-      });
-      flash.on('click', hideFlash);
-      textDiv = $('<div/>', {
-        "class": 'flash-text',
-        text: message
-      });
-      textDiv.appendTo(flash);
-      if (this.flashContainer.parent().hasClass('content-wrapper')) {
-        textDiv.addClass('container-fluid container-limited');
-      }
-      flash.appendTo(this.flashContainer);
-      this.flashContainer.show();
+  hideFlash = function() {
+    return $(this).fadeOut();
+  };
+
+  function Flash(message, type, parent) {
+    var flash, textDiv;
+    if (type == null) {
+      type = 'alert';
+    }
+    if (parent == null) {
+      parent = null;
+    }
+    if (parent) {
+      this.flashContainer = parent.find('.flash-container');
+    } else {
+      this.flashContainer = $('.flash-container-page');
+    }
+    this.flashContainer.html('');
+    flash = $('<div/>', {
+      "class": "flash-" + type
+    });
+    flash.on('click', hideFlash);
+    textDiv = $('<div/>', {
+      "class": 'flash-text',
+      text: message
+    });
+    textDiv.appendTo(flash);
+    if (this.flashContainer.parent().hasClass('content-wrapper')) {
+      textDiv.addClass('container-fluid container-limited');
     }
+    flash.appendTo(this.flashContainer);
+    this.flashContainer.show();
+  }
 
-    return Flash;
-  })();
-}).call(window);
+  return Flash;
+})();
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
new file mode 100644
index 0000000000000000000000000000000000000000..9ac4c49d697dbc5b05e4934e3b5573fb117b0d0b
--- /dev/null
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -0,0 +1,390 @@
+/* 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, vars-on-top, max-len */
+
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from '~/behaviors/gl_emoji';
+
+// Creates the variables for setting up GFM auto-completion
+window.gl = window.gl || {};
+
+function sanitize(str) {
+  return str.replace(/<(?:.|\n)*?>/gm, '');
+}
+
+window.gl.GfmAutoComplete = {
+  dataSources: {},
+  defaultLoadingData: ['loading'],
+  cachedData: {},
+  isLoadingData: {},
+  atTypeMap: {
+    ':': 'emojis',
+    '@': 'members',
+    '#': 'issues',
+    '!': 'mergeRequests',
+    '~': 'labels',
+    '%': 'milestones',
+    '/': 'commands'
+  },
+  // Emoji
+  Emoji: {
+    templateFunction: function(name) {
+      return `<li>
+        ${name} ${glEmojiTag(name)}
+      </li>
+      `;
+    }
+  },
+  // Team Members
+  Members: {
+    template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
+  },
+  Labels: {
+    template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+  },
+  // Issues and MergeRequests
+  Issues: {
+    template: '<li><small>${id}</small> ${title}</li>'
+  },
+  // Milestones
+  Milestones: {
+    template: '<li>${title}</li>'
+  },
+  Loading: {
+    template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+  },
+  DefaultOptions: {
+    sorter: function(query, items, searchKey) {
+      this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+      if (gl.GfmAutoComplete.isLoading(items)) {
+        this.setting.highlightFirst = false;
+        return items;
+      }
+      return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
+    },
+    filter: function(query, data, searchKey) {
+      if (gl.GfmAutoComplete.isLoading(data)) {
+        gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
+        return data;
+      } else {
+        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 + '"';
+      }
+      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, atSymbolsWithBar, atSymbolsWithoutBar;
+      atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+      atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+      subtext = subtext.split(/\s+/g).pop();
+      flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+
+      _a = decodeURI("%C3%80");
+      _y = decodeURI("%C3%BF");
+
+      regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
+
+      match = regexp.exec(subtext);
+
+      if (match) {
+        return match[1];
+      } else {
+        return null;
+      }
+    }
+  },
+  setup: function(input) {
+    // Add GFM auto-completion to all input fields, that accept GFM input.
+    this.input = input || $('.js-gfm-input');
+    this.setupLifecycle();
+  },
+  setupLifecycle() {
+    this.input.each((i, input) => {
+      const $input = $(input);
+      $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+      // This triggers at.js again
+      // Needed for slash commands with suffixes (ex: /label ~)
+      $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+    });
+  },
+  setupAtWho: function($input) {
+    // Emoji
+    $input.atwho({
+      at: ':',
+      displayTpl: function(value) {
+        return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
+      }.bind(this),
+      insertTpl: ':${name}:',
+      skipSpecialCharacterTest: true,
+      data: this.defaultLoadingData,
+      callbacks: {
+        sorter: this.DefaultOptions.sorter,
+        beforeInsert: this.DefaultOptions.beforeInsert,
+        filter: this.DefaultOptions.filter
+      }
+    });
+    // Team Members
+    $input.atwho({
+      at: '@',
+      displayTpl: function(value) {
+        return value.username != null ? this.Members.template : this.Loading.template;
+      }.bind(this),
+      insertTpl: '${atwho-at}${username}',
+      searchKey: 'search',
+      alwaysHighlightFirst: true,
+      skipSpecialCharacterTest: true,
+      data: this.defaultLoadingData,
+      callbacks: {
+        sorter: this.DefaultOptions.sorter,
+        filter: this.DefaultOptions.filter,
+        beforeInsert: this.DefaultOptions.beforeInsert,
+        matcher: this.DefaultOptions.matcher,
+        beforeSave: function(members) {
+          return $.map(members, function(m) {
+            let title = '';
+            if (m.username == null) {
+              return m;
+            }
+            title = m.name;
+            if (m.count) {
+              title += " (" + m.count + ")";
+            }
+
+            const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+            const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+            const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
+            return {
+              username: m.username,
+              avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+              title: sanitize(title),
+              search: sanitize(m.username + " " + m.name)
+            };
+          });
+        }
+      }
+    });
+    $input.atwho({
+      at: '#',
+      alias: 'issues',
+      searchKey: 'search',
+      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,
+        filter: this.DefaultOptions.filter,
+        beforeInsert: this.DefaultOptions.beforeInsert,
+        matcher: this.DefaultOptions.matcher,
+        beforeSave: function(issues) {
+          return $.map(issues, function(i) {
+            if (i.title == null) {
+              return i;
+            }
+            return {
+              id: i.iid,
+              title: sanitize(i.title),
+              search: i.iid + " " + i.title
+            };
+          });
+        }
+      }
+    });
+    $input.atwho({
+      at: '%',
+      alias: 'milestones',
+      searchKey: 'search',
+      insertTpl: '${atwho-at}${title}',
+      displayTpl: function(value) {
+        return value.title != null ? this.Milestones.template : this.Loading.template;
+      }.bind(this),
+      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) {
+              return m;
+            }
+            return {
+              id: m.iid,
+              title: sanitize(m.title),
+              search: "" + m.title
+            };
+          });
+        }
+      }
+    });
+    $input.atwho({
+      at: '!',
+      alias: 'mergerequests',
+      searchKey: 'search',
+      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,
+        filter: this.DefaultOptions.filter,
+        beforeInsert: this.DefaultOptions.beforeInsert,
+        matcher: this.DefaultOptions.matcher,
+        beforeSave: function(merges) {
+          return $.map(merges, function(m) {
+            if (m.title == null) {
+              return m;
+            }
+            return {
+              id: m.iid,
+              title: sanitize(m.title),
+              search: m.iid + " " + m.title
+            };
+          });
+        }
+      }
+    });
+    $input.atwho({
+      at: '~',
+      alias: 'labels',
+      searchKey: 'search',
+      data: this.defaultLoadingData,
+      displayTpl: function(value) {
+        return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+      }.bind(this),
+      insertTpl: '${atwho-at}${title}',
+      callbacks: {
+        matcher: this.DefaultOptions.matcher,
+        beforeInsert: this.DefaultOptions.beforeInsert,
+        filter: this.DefaultOptions.filter,
+        sorter: this.DefaultOptions.sorter,
+        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),
+              color: m.color,
+              search: "" + m.title
+            };
+          });
+        }
+      }
+    });
+    // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+    $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>';
+        }
+        if (value.params.length > 0) {
+          tpl += ' <small><%- params.join(" ") %></small>';
+        }
+        if (value.description !== '') {
+          tpl += '<small class="description"><i><%- description %></i></small>';
+        }
+        tpl += '</li>';
+        return _.template(tpl)(value);
+      }.bind(this),
+      insertTpl: function(value) {
+        var tpl = "/${name} ";
+        var reference_prefix = null;
+        if (value.params.length > 0) {
+          reference_prefix = value.params[0][0];
+          if (/^[@%~]/.test(reference_prefix)) {
+            tpl += '<%- reference_prefix %>';
+          }
+        }
+        return _.template(tpl)({ reference_prefix: reference_prefix });
+      },
+      suffix: '',
+      callbacks: {
+        sorter: this.DefaultOptions.sorter,
+        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) {
+              search = search + " " + c.aliases.join(" ");
+            }
+            return {
+              name: c.name,
+              aliases: c.aliases,
+              params: c.params,
+              description: c.description,
+              search: search
+            };
+          });
+        },
+        matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+          var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
+          var match = regexp.exec(subtext);
+          if (match) {
+            return match[1];
+          } else {
+            return null;
+          }
+        }
+      }
+    });
+    return;
+  },
+  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 if (this.atTypeMap[at] === 'emojis') {
+      this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
+    } else {
+      $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+        this.loadData($input, at, data);
+      }).fail(() => { this.isLoadingData[at] = false; });
+    }
+  },
+  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 $input.trigger('keyup');
+  },
+  isLoading(data) {
+    var dataToInspect = data;
+    if (data && data.length > 0) {
+      dataToInspect = data[0];
+    }
+
+    var loadingState = this.defaultLoadingData[0];
+    return dataToInspect &&
+      (dataToInspect === loadingState || dataToInspect.name === loadingState);
+  }
+};
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
deleted file mode 100644
index 60d6658dc16687e9e3d21aa18862f2d65e86ba81..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ /dev/null
@@ -1,383 +0,0 @@
-/* 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, vars-on-top, max-len */
-
-// Creates the variables for setting up GFM auto-completion
-(function() {
-  if (window.gl == null) {
-    window.gl = {};
-  }
-
-  function sanitize(str) {
-    return str.replace(/<(?:.|\n)*?>/gm, '');
-  }
-
-  window.gl.GfmAutoComplete = {
-    dataSources: {},
-    defaultLoadingData: ['loading'],
-    cachedData: {},
-    isLoadingData: {},
-    atTypeMap: {
-      ':': 'emojis',
-      '@': 'members',
-      '#': 'issues',
-      '!': 'mergeRequests',
-      '~': 'labels',
-      '%': 'milestones',
-      '/': 'commands'
-    },
-    // Emoji
-    Emoji: {
-      template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
-    },
-    // Team Members
-    Members: {
-      template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
-    },
-    Labels: {
-      template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
-    },
-    // Issues and MergeRequests
-    Issues: {
-      template: '<li><small>${id}</small> ${title}</li>'
-    },
-    // Milestones
-    Milestones: {
-      template: '<li>${title}</li>'
-    },
-    Loading: {
-      template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
-    },
-    DefaultOptions: {
-      sorter: function(query, items, searchKey) {
-        this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
-        if (gl.GfmAutoComplete.isLoading(items)) {
-          this.setting.highlightFirst = false;
-          return items;
-        }
-        return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
-      },
-      filter: function(query, data, searchKey) {
-        if (gl.GfmAutoComplete.isLoading(data)) {
-          gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
-          return data;
-        } else {
-          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 + '"';
-        }
-        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, atSymbolsWithBar, atSymbolsWithoutBar;
-        atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
-        atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
-        subtext = subtext.split(/\s+/g).pop();
-        flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-
-        _a = decodeURI("%C3%80");
-        _y = decodeURI("%C3%BF");
-
-        regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
-
-        match = regexp.exec(subtext);
-
-        if (match) {
-          return match[1];
-        } else {
-          return null;
-        }
-      }
-    },
-    setup: function(input) {
-      // Add GFM auto-completion to all input fields, that accept GFM input.
-      this.input = input || $('.js-gfm-input');
-      this.setupLifecycle();
-    },
-    setupLifecycle() {
-      this.input.each((i, input) => {
-        const $input = $(input);
-        $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
-        // This triggers at.js again
-        // Needed for slash commands with suffixes (ex: /label ~)
-        $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
-      });
-    },
-    setupAtWho: function($input) {
-      // Emoji
-      $input.atwho({
-        at: ':',
-        displayTpl: function(value) {
-          return value.path != null ? this.Emoji.template : this.Loading.template;
-        }.bind(this),
-        insertTpl: ':${name}:',
-        skipSpecialCharacterTest: true,
-        data: this.defaultLoadingData,
-        callbacks: {
-          sorter: this.DefaultOptions.sorter,
-          beforeInsert: this.DefaultOptions.beforeInsert,
-          filter: this.DefaultOptions.filter
-        }
-      });
-      // Team Members
-      $input.atwho({
-        at: '@',
-        displayTpl: function(value) {
-          return value.username != null ? this.Members.template : this.Loading.template;
-        }.bind(this),
-        insertTpl: '${atwho-at}${username}',
-        searchKey: 'search',
-        alwaysHighlightFirst: true,
-        skipSpecialCharacterTest: true,
-        data: this.defaultLoadingData,
-        callbacks: {
-          sorter: this.DefaultOptions.sorter,
-          filter: this.DefaultOptions.filter,
-          beforeInsert: this.DefaultOptions.beforeInsert,
-          matcher: this.DefaultOptions.matcher,
-          beforeSave: function(members) {
-            return $.map(members, function(m) {
-              let title = '';
-              if (m.username == null) {
-                return m;
-              }
-              title = m.name;
-              if (m.count) {
-                title += " (" + m.count + ")";
-              }
-
-              const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
-              const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
-              const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
-
-              return {
-                username: m.username,
-                avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
-                title: sanitize(title),
-                search: sanitize(m.username + " " + m.name)
-              };
-            });
-          }
-        }
-      });
-      $input.atwho({
-        at: '#',
-        alias: 'issues',
-        searchKey: 'search',
-        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,
-          filter: this.DefaultOptions.filter,
-          beforeInsert: this.DefaultOptions.beforeInsert,
-          matcher: this.DefaultOptions.matcher,
-          beforeSave: function(issues) {
-            return $.map(issues, function(i) {
-              if (i.title == null) {
-                return i;
-              }
-              return {
-                id: i.iid,
-                title: sanitize(i.title),
-                search: i.iid + " " + i.title
-              };
-            });
-          }
-        }
-      });
-      $input.atwho({
-        at: '%',
-        alias: 'milestones',
-        searchKey: 'search',
-        insertTpl: '${atwho-at}${title}',
-        displayTpl: function(value) {
-          return value.title != null ? this.Milestones.template : this.Loading.template;
-        }.bind(this),
-        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) {
-                return m;
-              }
-              return {
-                id: m.iid,
-                title: sanitize(m.title),
-                search: "" + m.title
-              };
-            });
-          }
-        }
-      });
-      $input.atwho({
-        at: '!',
-        alias: 'mergerequests',
-        searchKey: 'search',
-        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,
-          filter: this.DefaultOptions.filter,
-          beforeInsert: this.DefaultOptions.beforeInsert,
-          matcher: this.DefaultOptions.matcher,
-          beforeSave: function(merges) {
-            return $.map(merges, function(m) {
-              if (m.title == null) {
-                return m;
-              }
-              return {
-                id: m.iid,
-                title: sanitize(m.title),
-                search: m.iid + " " + m.title
-              };
-            });
-          }
-        }
-      });
-      $input.atwho({
-        at: '~',
-        alias: 'labels',
-        searchKey: 'search',
-        data: this.defaultLoadingData,
-        displayTpl: function(value) {
-          return this.isLoading(value) ? this.Loading.template : this.Labels.template;
-        }.bind(this),
-        insertTpl: '${atwho-at}${title}',
-        callbacks: {
-          matcher: this.DefaultOptions.matcher,
-          beforeInsert: this.DefaultOptions.beforeInsert,
-          filter: this.DefaultOptions.filter,
-          sorter: this.DefaultOptions.sorter,
-          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),
-                color: m.color,
-                search: "" + m.title
-              };
-            });
-          }
-        }
-      });
-      // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
-      $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>';
-          }
-          if (value.params.length > 0) {
-            tpl += ' <small><%- params.join(" ") %></small>';
-          }
-          if (value.description !== '') {
-            tpl += '<small class="description"><i><%- description %></i></small>';
-          }
-          tpl += '</li>';
-          return _.template(tpl)(value);
-        }.bind(this),
-        insertTpl: function(value) {
-          var tpl = "/${name} ";
-          var reference_prefix = null;
-          if (value.params.length > 0) {
-            reference_prefix = value.params[0][0];
-            if (/^[@%~]/.test(reference_prefix)) {
-              tpl += '<%- reference_prefix %>';
-            }
-          }
-          return _.template(tpl)({ reference_prefix: reference_prefix });
-        },
-        suffix: '',
-        callbacks: {
-          sorter: this.DefaultOptions.sorter,
-          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) {
-                search = search + " " + c.aliases.join(" ");
-              }
-              return {
-                name: c.name,
-                aliases: c.aliases,
-                params: c.params,
-                description: c.description,
-                search: search
-              };
-            });
-          },
-          matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
-            var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
-            var match = regexp.exec(subtext);
-            if (match) {
-              return match[1];
-            } else {
-              return null;
-            }
-          }
-        }
-      });
-      return;
-    },
-    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($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 $input.trigger('keyup');
-    },
-    isLoading(data) {
-      var dataToInspect = data;
-      if (data && data.length > 0) {
-        dataToInspect = data[0];
-      }
-
-      var loadingState = this.defaultLoadingData[0];
-      return dataToInspect &&
-        (dataToInspect === loadingState || dataToInspect.name === loadingState);
-    }
-  };
-}).call(window);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a01662e2f9ef76c65de6f26506eba9da8161449c..a03f1202a6d4c80ad26263129a4f3bfe7d7d7971 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,850 +1,848 @@
 /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
 /* global fuzzaldrinPlus */
 
-(function() {
-  var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
-    bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
-    indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
-
-  GitLabDropdownFilter = (function() {
-    var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
-
-    BLUR_KEYCODES = [27, 40];
-
-    ARROW_KEY_CODES = [38, 40];
-
-    HAS_VALUE_CLASS = "has-value";
-
-    function GitLabDropdownFilter(input, options) {
-      var $clearButton, $inputContainer, ref, timeout;
-      this.input = input;
-      this.options = options;
-      this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
-      $inputContainer = this.input.parent();
-      $clearButton = $inputContainer.find('.js-dropdown-input-clear');
-      $clearButton.on('click', (function(_this) {
-        // Clear click
-        return function(e) {
+var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
+  bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+  indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
+
+GitLabDropdownFilter = (function() {
+  var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
+
+  BLUR_KEYCODES = [27, 40];
+
+  ARROW_KEY_CODES = [38, 40];
+
+  HAS_VALUE_CLASS = "has-value";
+
+  function GitLabDropdownFilter(input, options) {
+    var $clearButton, $inputContainer, ref, timeout;
+    this.input = input;
+    this.options = options;
+    this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+    $inputContainer = this.input.parent();
+    $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+    $clearButton.on('click', (function(_this) {
+      // Clear click
+      return function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        return _this.input.val('').trigger('input').focus();
+      };
+    })(this));
+    // Key events
+    timeout = "";
+    this.input
+      .on('keydown', function (e) {
+        var keyCode = e.which;
+        if (keyCode === 13 && !options.elIsInput) {
           e.preventDefault();
-          e.stopPropagation();
-          return _this.input.val('').trigger('input').focus();
-        };
-      })(this));
-      // Key events
-      timeout = "";
-      this.input
-        .on('keydown', function (e) {
-          var keyCode = e.which;
-          if (keyCode === 13 && !options.elIsInput) {
-            e.preventDefault();
-          }
-        })
-        .on('input', function() {
-          if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
-            $inputContainer.addClass(HAS_VALUE_CLASS);
-          } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
-            $inputContainer.removeClass(HAS_VALUE_CLASS);
-          }
-          // Only filter asynchronously only if option remote is set
-          if (this.options.remote) {
-            clearTimeout(timeout);
-            return timeout = setTimeout(function() {
-              $inputContainer.parent().addClass('is-loading');
-
-              return this.options.query(this.input.val(), function(data) {
-                $inputContainer.parent().removeClass('is-loading');
-                return this.options.callback(data);
-              }.bind(this));
-            }.bind(this), 250);
-          } else {
-            return this.filter(this.input.val());
-          }
-        }.bind(this));
-    }
+        }
+      })
+      .on('input', function() {
+        if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+          $inputContainer.addClass(HAS_VALUE_CLASS);
+        } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+          $inputContainer.removeClass(HAS_VALUE_CLASS);
+        }
+        // Only filter asynchronously only if option remote is set
+        if (this.options.remote) {
+          clearTimeout(timeout);
+          return timeout = setTimeout(function() {
+            $inputContainer.parent().addClass('is-loading');
+
+            return this.options.query(this.input.val(), function(data) {
+              $inputContainer.parent().removeClass('is-loading');
+              return this.options.callback(data);
+            }.bind(this));
+          }.bind(this), 250);
+        } else {
+          return this.filter(this.input.val());
+        }
+      }.bind(this));
+  }
 
-    GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
-      return BLUR_KEYCODES.indexOf(keyCode) >= 0;
-    };
+  GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
+    return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+  };
 
-    GitLabDropdownFilter.prototype.filter = function(search_text) {
-      var data, elements, group, key, results, tmp;
-      if (this.options.onFilter) {
-        this.options.onFilter(search_text);
-      }
-      data = this.options.data();
-      if ((data != null) && !this.options.filterByText) {
-        results = data;
-        if (search_text !== '') {
-          // When data is an array of objects therefore [object Array] e.g.
-          // [
-          //   { prop: 'foo' },
-          //   { prop: 'baz' }
-          // ]
-          if (_.isArray(data)) {
-            results = fuzzaldrinPlus.filter(data, search_text, {
-              key: this.options.keys
-            });
-          } else {
-            // If data is grouped therefore an [object Object]. e.g.
-            // {
-            //   groupName1: [
-            //     { prop: 'foo' },
-            //     { prop: 'baz' }
-            //   ],
-            //   groupName2: [
-            //     { prop: 'abc' },
-            //     { prop: 'def' }
-            //   ]
-            // }
-            if (gl.utils.isObject(data)) {
-              results = {};
-              for (key in data) {
-                group = data[key];
-                tmp = fuzzaldrinPlus.filter(group, search_text, {
-                  key: this.options.keys
+  GitLabDropdownFilter.prototype.filter = function(search_text) {
+    var data, elements, group, key, results, tmp;
+    if (this.options.onFilter) {
+      this.options.onFilter(search_text);
+    }
+    data = this.options.data();
+    if ((data != null) && !this.options.filterByText) {
+      results = data;
+      if (search_text !== '') {
+        // When data is an array of objects therefore [object Array] e.g.
+        // [
+        //   { prop: 'foo' },
+        //   { prop: 'baz' }
+        // ]
+        if (_.isArray(data)) {
+          results = fuzzaldrinPlus.filter(data, search_text, {
+            key: this.options.keys
+          });
+        } else {
+          // If data is grouped therefore an [object Object]. e.g.
+          // {
+          //   groupName1: [
+          //     { prop: 'foo' },
+          //     { prop: 'baz' }
+          //   ],
+          //   groupName2: [
+          //     { prop: 'abc' },
+          //     { prop: 'def' }
+          //   ]
+          // }
+          if (gl.utils.isObject(data)) {
+            results = {};
+            for (key in data) {
+              group = data[key];
+              tmp = fuzzaldrinPlus.filter(group, search_text, {
+                key: this.options.keys
+              });
+              if (tmp.length) {
+                results[key] = tmp.map(function(item) {
+                  return item;
                 });
-                if (tmp.length) {
-                  results[key] = tmp.map(function(item) {
-                    return item;
-                  });
-                }
               }
             }
           }
         }
-        return this.options.callback(results);
-      } else {
-        elements = this.options.elements();
-        if (search_text) {
-          return elements.each(function() {
-            var $el, matches;
-            $el = $(this);
-            matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
-            if (!$el.is('.dropdown-header')) {
-              if (matches.length) {
-                return $el.show().removeClass('option-hidden');
-              } else {
-                return $el.hide().addClass('option-hidden');
-              }
+      }
+      return this.options.callback(results);
+    } else {
+      elements = this.options.elements();
+      if (search_text) {
+        return elements.each(function() {
+          var $el, matches;
+          $el = $(this);
+          matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
+          if (!$el.is('.dropdown-header')) {
+            if (matches.length) {
+              return $el.show().removeClass('option-hidden');
+            } else {
+              return $el.hide().addClass('option-hidden');
             }
-          });
-        } else {
-          return elements.show().removeClass('option-hidden');
-        }
+          }
+        });
+      } else {
+        return elements.show().removeClass('option-hidden');
       }
-    };
-
-    return GitLabDropdownFilter;
-  })();
+    }
+  };
 
-  GitLabDropdownRemote = (function() {
-    function GitLabDropdownRemote(dataEndpoint, options) {
-      this.dataEndpoint = dataEndpoint;
-      this.options = options;
+  return GitLabDropdownFilter;
+})();
+
+GitLabDropdownRemote = (function() {
+  function GitLabDropdownRemote(dataEndpoint, options) {
+    this.dataEndpoint = dataEndpoint;
+    this.options = options;
+  }
+
+  GitLabDropdownRemote.prototype.execute = function() {
+    if (typeof this.dataEndpoint === "string") {
+      return this.fetchData();
+    } else if (typeof this.dataEndpoint === "function") {
+      if (this.options.beforeSend) {
+        this.options.beforeSend();
+      }
+      return this.dataEndpoint("", (function(_this) {
+        // Fetch the data by calling the data funcfion
+        return function(data) {
+          if (_this.options.success) {
+            _this.options.success(data);
+          }
+          if (_this.options.beforeSend) {
+            return _this.options.beforeSend();
+          }
+        };
+      })(this));
     }
+  };
 
-    GitLabDropdownRemote.prototype.execute = function() {
-      if (typeof this.dataEndpoint === "string") {
-        return this.fetchData();
-      } else if (typeof this.dataEndpoint === "function") {
-        if (this.options.beforeSend) {
-          this.options.beforeSend();
-        }
-        return this.dataEndpoint("", (function(_this) {
-          // Fetch the data by calling the data funcfion
-          return function(data) {
-            if (_this.options.success) {
-              _this.options.success(data);
-            }
-            if (_this.options.beforeSend) {
-              return _this.options.beforeSend();
-            }
-          };
-        })(this));
-      }
-    };
+  GitLabDropdownRemote.prototype.fetchData = function() {
+    return $.ajax({
+      url: this.dataEndpoint,
+      dataType: this.options.dataType,
+      beforeSend: (function(_this) {
+        return function() {
+          if (_this.options.beforeSend) {
+            return _this.options.beforeSend();
+          }
+        };
+      })(this),
+      success: (function(_this) {
+        return function(data) {
+          if (_this.options.success) {
+            return _this.options.success(data);
+          }
+        };
+      })(this)
+    });
+  // Fetch the data through ajax if the data is a string
+  };
 
-    GitLabDropdownRemote.prototype.fetchData = function() {
-      return $.ajax({
-        url: this.dataEndpoint,
-        dataType: this.options.dataType,
-        beforeSend: (function(_this) {
-          return function() {
-            if (_this.options.beforeSend) {
-              return _this.options.beforeSend();
-            }
-          };
-        })(this),
-        success: (function(_this) {
-          return function(data) {
-            if (_this.options.success) {
-              return _this.options.success(data);
-            }
-          };
-        })(this)
-      });
-    // Fetch the data through ajax if the data is a string
-    };
+  return GitLabDropdownRemote;
+})();
 
-    return GitLabDropdownRemote;
-  })();
+GitLabDropdown = (function() {
+  var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
 
-  GitLabDropdown = (function() {
-    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";
 
-    LOADING_CLASS = "is-loading";
+  PAGE_TWO_CLASS = "is-page-two";
 
-    PAGE_TWO_CLASS = "is-page-two";
+  ACTIVE_CLASS = "is-active";
 
-    ACTIVE_CLASS = "is-active";
+  INDETERMINATE_CLASS = "is-indeterminate";
 
-    INDETERMINATE_CLASS = "is-indeterminate";
+  currentIndex = -1;
 
-    currentIndex = -1;
+  NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
 
-    NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
-
-    SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
-
-    CURSOR_SELECT_SCROLL_PADDING = 5;
-
-    FILTER_INPUT = '.dropdown-input .dropdown-input-field';
-
-    function GitLabDropdown(el1, options) {
-      var searchFields, selector, self;
-      this.el = el1;
-      this.options = options;
-      this.updateLabel = bind(this.updateLabel, this);
-      this.hidden = bind(this.hidden, this);
-      this.opened = bind(this.opened, this);
-      this.shouldPropagate = bind(this.shouldPropagate, this);
-      self = this;
-      selector = $(this.el).data("target");
-      this.dropdown = selector != null ? $(selector) : $(this.el).parent();
-      // Set Defaults
-      this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
-      this.highlight = !!this.options.highlight;
-      this.filterInputBlur = this.options.filterInputBlur != null
-        ? this.options.filterInputBlur
-        : true;
-      // If no input is passed create a default one
-      self = this;
-      // If selector was passed
-      if (_.isString(this.filterInput)) {
-        this.filterInput = this.getElement(this.filterInput);
-      }
-      searchFields = this.options.search ? this.options.search.fields : [];
-      if (this.options.data) {
-        // If we provided data
-        // data could be an array of objects or a group of arrays
-        if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
-          this.fullData = this.options.data;
-          currentIndex = -1;
-          this.parseData(this.options.data);
-          this.focusTextInput();
-        } else {
-          this.remote = new GitLabDropdownRemote(this.options.data, {
-            dataType: this.options.dataType,
-            beforeSend: this.toggleLoading.bind(this),
-            success: (function(_this) {
-              return function(data) {
-                _this.fullData = data;
-                _this.parseData(_this.fullData);
-                _this.focusTextInput();
-                if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
-                  return _this.filter.input.trigger('input');
-                }
-              };
-            // Remote data
-            })(this)
-          });
-        }
-      }
-      // Init filterable
-      if (this.options.filterable) {
-        this.filter = new GitLabDropdownFilter(this.filterInput, {
-          elIsInput: $(this.el).is('input'),
-          filterInputBlur: this.filterInputBlur,
-          filterByText: this.options.filterByText,
-          onFilter: this.options.onFilter,
-          remote: this.options.filterRemote,
-          query: this.options.data,
-          keys: searchFields,
-          elements: (function(_this) {
-            return function() {
-              selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
-              if (_this.dropdown.find('.dropdown-toggle-page').length) {
-                selector = ".dropdown-page-one " + selector;
-              }
-              return $(selector);
-            };
-          })(this),
-          data: (function(_this) {
-            return function() {
-              return _this.fullData;
-            };
-          })(this),
-          callback: (function(_this) {
+  SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+
+  CURSOR_SELECT_SCROLL_PADDING = 5;
+
+  FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+
+  function GitLabDropdown(el1, options) {
+    var searchFields, selector, self;
+    this.el = el1;
+    this.options = options;
+    this.updateLabel = bind(this.updateLabel, this);
+    this.hidden = bind(this.hidden, this);
+    this.opened = bind(this.opened, this);
+    this.shouldPropagate = bind(this.shouldPropagate, this);
+    self = this;
+    selector = $(this.el).data("target");
+    this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+    // Set Defaults
+    this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+    this.highlight = !!this.options.highlight;
+    this.filterInputBlur = this.options.filterInputBlur != null
+      ? this.options.filterInputBlur
+      : true;
+    // If no input is passed create a default one
+    self = this;
+    // If selector was passed
+    if (_.isString(this.filterInput)) {
+      this.filterInput = this.getElement(this.filterInput);
+    }
+    searchFields = this.options.search ? this.options.search.fields : [];
+    if (this.options.data) {
+      // If we provided data
+      // data could be an array of objects or a group of arrays
+      if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+        this.fullData = this.options.data;
+        currentIndex = -1;
+        this.parseData(this.options.data);
+        this.focusTextInput();
+      } else {
+        this.remote = new GitLabDropdownRemote(this.options.data, {
+          dataType: this.options.dataType,
+          beforeSend: this.toggleLoading.bind(this),
+          success: (function(_this) {
             return function(data) {
-              _this.parseData(data);
-              if (_this.filterInput.val() !== '') {
-                selector = SELECTABLE_CLASSES;
-                if (_this.dropdown.find('.dropdown-toggle-page').length) {
-                  selector = ".dropdown-page-one " + selector;
-                }
-                if ($(_this.el).is('input')) {
-                  currentIndex = -1;
-                } else {
-                  $(selector, _this.dropdown).first().find('a').addClass('is-focused');
-                  currentIndex = 0;
-                }
+              _this.fullData = data;
+              _this.parseData(_this.fullData);
+              _this.focusTextInput();
+              if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
+                return _this.filter.input.trigger('input');
               }
             };
+          // Remote data
           })(this)
         });
       }
-      // Event listeners
-      this.dropdown.on("shown.bs.dropdown", this.opened);
-      this.dropdown.on("hidden.bs.dropdown", this.hidden);
-      $(this.el).on("update.label", this.updateLabel);
-      this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
-      this.dropdown.on('keyup', (function(_this) {
-        return function(e) {
-          // Escape key
-          if (e.which === 27) {
-            return $('.dropdown-menu-close', _this.dropdown).trigger('click');
-          }
-        };
-      })(this));
-      this.dropdown.on('blur', 'a', (function(_this) {
-        return function(e) {
-          var $dropdownMenu, $relatedTarget;
-          if (e.relatedTarget != null) {
-            $relatedTarget = $(e.relatedTarget);
-            $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
-            if ($dropdownMenu.length === 0) {
-              return _this.dropdown.removeClass('open');
+    }
+    // Init filterable
+    if (this.options.filterable) {
+      this.filter = new GitLabDropdownFilter(this.filterInput, {
+        elIsInput: $(this.el).is('input'),
+        filterInputBlur: this.filterInputBlur,
+        filterByText: this.options.filterByText,
+        onFilter: this.options.onFilter,
+        remote: this.options.filterRemote,
+        query: this.options.data,
+        keys: searchFields,
+        elements: (function(_this) {
+          return function() {
+            selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
+            if (_this.dropdown.find('.dropdown-toggle-page').length) {
+              selector = ".dropdown-page-one " + selector;
+            }
+            return $(selector);
+          };
+        })(this),
+        data: (function(_this) {
+          return function() {
+            return _this.fullData;
+          };
+        })(this),
+        callback: (function(_this) {
+          return function(data) {
+            _this.parseData(data);
+            if (_this.filterInput.val() !== '') {
+              selector = SELECTABLE_CLASSES;
+              if (_this.dropdown.find('.dropdown-toggle-page').length) {
+                selector = ".dropdown-page-one " + selector;
+              }
+              if ($(_this.el).is('input')) {
+                currentIndex = -1;
+              } else {
+                $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+                currentIndex = 0;
+              }
             }
+          };
+        })(this)
+      });
+    }
+    // Event listeners
+    this.dropdown.on("shown.bs.dropdown", this.opened);
+    this.dropdown.on("hidden.bs.dropdown", this.hidden);
+    $(this.el).on("update.label", this.updateLabel);
+    this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
+    this.dropdown.on('keyup', (function(_this) {
+      return function(e) {
+        // Escape key
+        if (e.which === 27) {
+          return $('.dropdown-menu-close', _this.dropdown).trigger('click');
+        }
+      };
+    })(this));
+    this.dropdown.on('blur', 'a', (function(_this) {
+      return function(e) {
+        var $dropdownMenu, $relatedTarget;
+        if (e.relatedTarget != null) {
+          $relatedTarget = $(e.relatedTarget);
+          $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+          if ($dropdownMenu.length === 0) {
+            return _this.dropdown.removeClass('open');
           }
+        }
+      };
+    })(this));
+    if (this.dropdown.find(".dropdown-toggle-page").length) {
+      this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
+        return function(e) {
+          e.preventDefault();
+          e.stopPropagation();
+          return _this.togglePage();
         };
       })(this));
+    }
+    if (this.options.selectable) {
+      selector = ".dropdown-content a";
       if (this.dropdown.find(".dropdown-toggle-page").length) {
-        this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
-          return function(e) {
-            e.preventDefault();
-            e.stopPropagation();
-            return _this.togglePage();
-          };
-        })(this));
-      }
-      if (this.options.selectable) {
-        selector = ".dropdown-content a";
-        if (this.dropdown.find(".dropdown-toggle-page").length) {
-          selector = ".dropdown-page-one .dropdown-content a";
+        selector = ".dropdown-page-one .dropdown-content a";
+      }
+      this.dropdown.on("click", selector, function(e) {
+        var $el, selected, selectedObj, isMarking;
+        $el = $(this);
+        selected = self.rowClicked($el);
+        selectedObj = selected ? selected[0] : null;
+        isMarking = selected ? selected[1] : null;
+        if (self.options.clicked) {
+          self.options.clicked(selectedObj, $el, e, isMarking);
         }
-        this.dropdown.on("click", selector, function(e) {
-          var $el, selected, selectedObj, isMarking;
-          $el = $(this);
-          selected = self.rowClicked($el);
-          selectedObj = selected ? selected[0] : null;
-          isMarking = selected ? selected[1] : null;
-          if (self.options.clicked) {
-            self.options.clicked(selectedObj, $el, e, isMarking);
-          }
 
-          // Update label right after all modifications in dropdown has been done
-          if (self.options.toggleLabel) {
-            self.updateLabel(selectedObj, $el, self);
-          }
+        // Update label right after all modifications in dropdown has been done
+        if (self.options.toggleLabel) {
+          self.updateLabel(selectedObj, $el, self);
+        }
 
-          $el.trigger('blur');
-        });
-      }
+        $el.trigger('blur');
+      });
     }
+  }
 
-    // Finds an element inside wrapper element
-    GitLabDropdown.prototype.getElement = function(selector) {
-      return this.dropdown.find(selector);
-    };
+  // Finds an element inside wrapper element
+  GitLabDropdown.prototype.getElement = function(selector) {
+    return this.dropdown.find(selector);
+  };
 
-    GitLabDropdown.prototype.toggleLoading = function() {
-      return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
-    };
+  GitLabDropdown.prototype.toggleLoading = function() {
+    return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+  };
 
-    GitLabDropdown.prototype.togglePage = function() {
-      var menu;
-      menu = $('.dropdown-menu', this.dropdown);
-      if (menu.hasClass(PAGE_TWO_CLASS)) {
-        if (this.remote) {
-          this.remote.execute();
-        }
-      }
-      menu.toggleClass(PAGE_TWO_CLASS);
-      // Focus first visible input on active page
-      return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
-    };
-
-    GitLabDropdown.prototype.parseData = function(data) {
-      var full_html, groupData, html, name;
-      this.renderedData = data;
-      if (this.options.filterable && data.length === 0) {
-        // render no matching results
-        html = [this.noResults()];
-      } else {
-        // Handle array groups
-        if (gl.utils.isObject(data)) {
-          html = [];
-          for (name in data) {
-            groupData = data[name];
-            html.push(this.renderItem({
-              header: name
-            // Add header for each group
-            }, name));
-            this.renderData(groupData, name).map(function(item) {
-              return html.push(item);
-            });
-          }
-        } else {
-          // Render each row
-          html = this.renderData(data);
-        }
-      }
-      // Render the full menu
-      full_html = this.renderMenu(html);
-      return this.appendMenu(full_html);
-    };
-
-    GitLabDropdown.prototype.renderData = function(data, group) {
-      if (group == null) {
-        group = false;
+  GitLabDropdown.prototype.togglePage = function() {
+    var menu;
+    menu = $('.dropdown-menu', this.dropdown);
+    if (menu.hasClass(PAGE_TWO_CLASS)) {
+      if (this.remote) {
+        this.remote.execute();
       }
-      return data.map((function(_this) {
-        return function(obj, index) {
-          return _this.renderItem(obj, group, index);
-        };
-      })(this));
-    };
-
-    GitLabDropdown.prototype.shouldPropagate = function(e) {
-      var $target;
-      if (this.options.multiSelect) {
-        $target = $(e.target);
-        if ($target && !$target.hasClass('dropdown-menu-close') &&
-                       !$target.hasClass('dropdown-menu-close-icon') &&
-                       !$target.data('is-link')) {
-          e.stopPropagation();
-          return false;
-        } else {
-          return true;
+    }
+    menu.toggleClass(PAGE_TWO_CLASS);
+    // Focus first visible input on active page
+    return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+  };
+
+  GitLabDropdown.prototype.parseData = function(data) {
+    var full_html, groupData, html, name;
+    this.renderedData = data;
+    if (this.options.filterable && data.length === 0) {
+      // render no matching results
+      html = [this.noResults()];
+    } else {
+      // Handle array groups
+      if (gl.utils.isObject(data)) {
+        html = [];
+        for (name in data) {
+          groupData = data[name];
+          html.push(this.renderItem({
+            header: name
+          // Add header for each group
+          }, name));
+          this.renderData(groupData, name).map(function(item) {
+            return html.push(item);
+          });
         }
+      } else {
+        // Render each row
+        html = this.renderData(data);
       }
-    };
+    }
+    // Render the full menu
+    full_html = this.renderMenu(html);
+    return this.appendMenu(full_html);
+  };
 
-    GitLabDropdown.prototype.opened = function(e) {
-      var contentHtml;
-      this.resetRows();
-      this.addArrowKeyEvent();
+  GitLabDropdown.prototype.renderData = function(data, group) {
+    if (group == null) {
+      group = false;
+    }
+    return data.map((function(_this) {
+      return function(obj, index) {
+        return _this.renderItem(obj, group, index);
+      };
+    })(this));
+  };
 
-      // Makes indeterminate items effective
-      if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
-        this.parseData(this.fullData);
-      }
-      contentHtml = $('.dropdown-content', this.dropdown).html();
-      if (this.remote && contentHtml === "") {
-        this.remote.execute();
+  GitLabDropdown.prototype.shouldPropagate = function(e) {
+    var $target;
+    if (this.options.multiSelect) {
+      $target = $(e.target);
+      if ($target && !$target.hasClass('dropdown-menu-close') &&
+                     !$target.hasClass('dropdown-menu-close-icon') &&
+                     !$target.data('is-link')) {
+        e.stopPropagation();
+        return false;
       } else {
-        this.focusTextInput();
+        return true;
       }
+    }
+  };
 
-      if (this.options.showMenuAbove) {
-        this.positionMenuAbove();
-      }
+  GitLabDropdown.prototype.opened = function(e) {
+    var contentHtml;
+    this.resetRows();
+    this.addArrowKeyEvent();
 
-      if (this.options.opened) {
-        this.options.opened.call(this, e);
-      }
+    // Makes indeterminate items effective
+    if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
+      this.parseData(this.fullData);
+    }
+    contentHtml = $('.dropdown-content', this.dropdown).html();
+    if (this.remote && contentHtml === "") {
+      this.remote.execute();
+    } else {
+      this.focusTextInput();
+    }
+
+    if (this.options.showMenuAbove) {
+      this.positionMenuAbove();
+    }
 
-      return this.dropdown.trigger('shown.gl.dropdown');
-    };
+    if (this.options.opened) {
+      this.options.opened.call(this, e);
+    }
 
-    GitLabDropdown.prototype.positionMenuAbove = function() {
-      var $button = $(this.el);
-      var $menu = this.dropdown.find('.dropdown-menu');
+    return this.dropdown.trigger('shown.gl.dropdown');
+  };
 
-      $menu.css('top', ($button.height() + $menu.height()) * -1);
-    };
+  GitLabDropdown.prototype.positionMenuAbove = function() {
+    var $button = $(this.el);
+    var $menu = this.dropdown.find('.dropdown-menu');
 
-    GitLabDropdown.prototype.hidden = function(e) {
-      var $input;
-      this.resetRows();
-      this.removeArrayKeyEvent();
-      $input = this.dropdown.find(".dropdown-input-field");
-      if (this.options.filterable) {
-        $input.blur();
-      }
-      if (this.dropdown.find(".dropdown-toggle-page").length) {
-        $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
-      }
-      if (this.options.hidden) {
-        this.options.hidden.call(this, e);
-      }
-      return this.dropdown.trigger('hidden.gl.dropdown');
-    };
+    $menu.css('top', ($button.height() + $menu.height()) * -1);
+  };
 
-    // Render the full menu
-    GitLabDropdown.prototype.renderMenu = function(html) {
-      if (this.options.renderMenu) {
-        return this.options.renderMenu(html);
-      } else {
-        var ul = document.createElement('ul');
+  GitLabDropdown.prototype.hidden = function(e) {
+    var $input;
+    this.resetRows();
+    this.removeArrayKeyEvent();
+    $input = this.dropdown.find(".dropdown-input-field");
+    if (this.options.filterable) {
+      $input.blur();
+    }
+    if (this.dropdown.find(".dropdown-toggle-page").length) {
+      $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+    }
+    if (this.options.hidden) {
+      this.options.hidden.call(this, e);
+    }
+    return this.dropdown.trigger('hidden.gl.dropdown');
+  };
 
-        for (var i = 0; i < html.length; i += 1) {
-          var el = html[i];
+  // Render the full menu
+  GitLabDropdown.prototype.renderMenu = function(html) {
+    if (this.options.renderMenu) {
+      return this.options.renderMenu(html);
+    } else {
+      var ul = document.createElement('ul');
 
-          if (el instanceof jQuery) {
-            el = el.get(0);
-          }
+      for (var i = 0; i < html.length; i += 1) {
+        var el = html[i];
 
-          if (typeof el === 'string') {
-            ul.innerHTML += el;
-          } else {
-            ul.appendChild(el);
-          }
+        if (el instanceof jQuery) {
+          el = el.get(0);
         }
 
-        return ul;
+        if (typeof el === 'string') {
+          ul.innerHTML += el;
+        } else {
+          ul.appendChild(el);
+        }
       }
-    };
 
-    // Append the menu into the dropdown
-    GitLabDropdown.prototype.appendMenu = function(html) {
-      return this.clearMenu().append(html);
-    };
+      return ul;
+    }
+  };
+
+  // Append the menu into the dropdown
+  GitLabDropdown.prototype.appendMenu = function(html) {
+    return this.clearMenu().append(html);
+  };
 
-    GitLabDropdown.prototype.clearMenu = function() {
-      var selector;
-      selector = '.dropdown-content';
-      if (this.dropdown.find(".dropdown-toggle-page").length) {
-        selector = ".dropdown-page-one .dropdown-content";
-      }
+  GitLabDropdown.prototype.clearMenu = function() {
+    var selector;
+    selector = '.dropdown-content';
+    if (this.dropdown.find(".dropdown-toggle-page").length) {
+      selector = ".dropdown-page-one .dropdown-content";
+    }
 
-      return $(selector, this.dropdown).empty();
-    };
+    return $(selector, this.dropdown).empty();
+  };
 
-    GitLabDropdown.prototype.renderItem = function(data, group, index) {
-      var field, fieldName, html, selected, text, url, value;
-      if (group == null) {
-        group = false;
+  GitLabDropdown.prototype.renderItem = function(data, group, index) {
+    var field, fieldName, html, selected, text, url, value;
+    if (group == null) {
+      group = false;
+    }
+    if (index == null) {
+      // Render the row
+      index = false;
+    }
+    html = document.createElement('li');
+    if (data === 'divider' || data === 'separator') {
+      html.className = data;
+      return html;
+    }
+    // Header
+    if (data.header != null) {
+      html.className = 'dropdown-header';
+      html.innerHTML = data.header;
+      return html;
+    }
+    if (this.options.renderRow) {
+      // Call the render function
+      html = this.options.renderRow.call(this.options, data, this);
+    } else {
+      if (!selected) {
+        value = this.options.id ? this.options.id(data) : data.id;
+        fieldName = this.options.fieldName;
+
+        if (value) { value = value.toString().replace(/'/g, '\\\''); }
+
+        field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+        if (field.length) {
+          selected = true;
+        }
       }
-      if (index == null) {
-        // Render the row
-        index = false;
+      // Set URL
+      if (this.options.url != null) {
+        url = this.options.url(data);
+      } else {
+        url = data.url != null ? data.url : '#';
       }
-      html = document.createElement('li');
-      if (data === 'divider' || data === 'separator') {
-        html.className = data;
-        return html;
+      // Set Text
+      if (this.options.text != null) {
+        text = this.options.text(data);
+      } else {
+        text = data.text != null ? data.text : '';
       }
-      // Header
-      if (data.header != null) {
-        html.className = 'dropdown-header';
-        html.innerHTML = data.header;
-        return html;
+      if (this.highlight) {
+        text = this.highlightTextMatches(text, this.filterInput.val());
       }
-      if (this.options.renderRow) {
-        // Call the render function
-        html = this.options.renderRow.call(this.options, data, this);
-      } else {
-        if (!selected) {
-          value = this.options.id ? this.options.id(data) : data.id;
-          fieldName = this.options.fieldName;
-
-          if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
-          field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
-          if (field.length) {
-            selected = true;
-          }
-        }
-        // Set URL
-        if (this.options.url != null) {
-          url = this.options.url(data);
-        } else {
-          url = data.url != null ? data.url : '#';
-        }
-        // Set Text
-        if (this.options.text != null) {
-          text = this.options.text(data);
-        } else {
-          text = data.text != null ? data.text : '';
-        }
-        if (this.highlight) {
-          text = this.highlightTextMatches(text, this.filterInput.val());
-        }
-        // Create the list item & the link
-        var link = document.createElement('a');
-
-        link.href = url;
-        link.innerHTML = text;
+      // Create the list item & the link
+      var link = document.createElement('a');
 
-        if (selected) {
-          link.className = 'is-active';
-        }
-
-        if (group) {
-          link.dataset.group = group;
-          link.dataset.index = index;
-        }
+      link.href = url;
+      link.innerHTML = text;
 
-        html.appendChild(link);
+      if (selected) {
+        link.className = 'is-active';
       }
-      return html;
-    };
-
-    GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
-      var occurrences;
-      occurrences = fuzzaldrinPlus.match(text, term);
-      return text.split('').map(function(character, i) {
-        if (indexOf.call(occurrences, i) >= 0) {
-          return "<b>" + character + "</b>";
-        } else {
-          return character;
-        }
-      }).join('');
-    };
-
-    GitLabDropdown.prototype.noResults = function() {
-      var html;
-      return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
-    };
-
-    GitLabDropdown.prototype.rowClicked = function(el) {
-      var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
-
-      fieldName = this.options.fieldName;
-      isInput = $(this.el).is('input');
-      if (this.renderedData) {
-        groupName = el.data('group');
-        if (groupName) {
-          selectedIndex = el.data('index');
-          selectedObject = this.renderedData[groupName][selectedIndex];
-        } else {
-          selectedIndex = el.closest('li').index();
-          selectedObject = this.renderedData[selectedIndex];
-        }
+
+      if (group) {
+        link.dataset.group = group;
+        link.dataset.index = index;
       }
 
-      if (this.options.vue) {
-        if (el.hasClass(ACTIVE_CLASS)) {
-          el.removeClass(ACTIVE_CLASS);
-        } else {
-          el.addClass(ACTIVE_CLASS);
-        }
+      html.appendChild(link);
+    }
+    return html;
+  };
 
-        return [selectedObject];
+  GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
+    var occurrences;
+    occurrences = fuzzaldrinPlus.match(text, term);
+    return text.split('').map(function(character, i) {
+      if (indexOf.call(occurrences, i) !== -1) {
+        return "<b>" + character + "</b>";
+      } else {
+        return character;
       }
+    }).join('');
+  };
 
-      field = [];
-      value = this.options.id
-        ? this.options.id(selectedObject, el)
-        : selectedObject.id;
-      if (isInput) {
-        field = $(this.el);
-      } else if (value) {
-        field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
-      }
+  GitLabDropdown.prototype.noResults = function() {
+    var html;
+    return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+  };
+
+  GitLabDropdown.prototype.rowClicked = function(el) {
+    var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
 
-      if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
-        return;
+    fieldName = this.options.fieldName;
+    isInput = $(this.el).is('input');
+    if (this.renderedData) {
+      groupName = el.data('group');
+      if (groupName) {
+        selectedIndex = el.data('index');
+        selectedObject = this.renderedData[groupName][selectedIndex];
+      } else {
+        selectedIndex = el.closest('li').index();
+        selectedObject = this.renderedData[selectedIndex];
       }
+    }
 
+    if (this.options.vue) {
       if (el.hasClass(ACTIVE_CLASS)) {
-        isMarking = false;
         el.removeClass(ACTIVE_CLASS);
-        if (field && field.length) {
-          this.clearField(field, isInput);
-        }
-      } else if (el.hasClass(INDETERMINATE_CLASS)) {
-        isMarking = true;
-        el.addClass(ACTIVE_CLASS);
-        el.removeClass(INDETERMINATE_CLASS);
-        if (field && field.length && value == null) {
-          this.clearField(field, isInput);
-        }
-        if ((!field || !field.length) && fieldName) {
-          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) {
-            this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
-          }
-        }
-        if (field && field.length && value == null) {
-          this.clearField(field, isInput);
-        }
-        // Toggle active class for the tick mark
         el.addClass(ACTIVE_CLASS);
-        if (value != null) {
-          if ((!field || !field.length) && fieldName) {
-            this.addInput(fieldName, value, selectedObject);
-          } else if (field && field.length) {
-            field.val(value).trigger('change');
-          }
-        }
       }
 
-      return [selectedObject, isMarking];
-    };
+      return [selectedObject];
+    }
 
-    GitLabDropdown.prototype.focusTextInput = function() {
-      if (this.options.filterable) { this.filterInput.focus(); }
-    };
+    field = [];
+    value = this.options.id
+      ? this.options.id(selectedObject, el)
+      : selectedObject.id;
+    if (isInput) {
+      field = $(this.el);
+    } else if (value) {
+      field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
+    }
 
-    GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
-      var $input;
-      // Create hidden input for form
-      $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
-      if (this.options.inputId != null) {
-        $input.attr('id', this.options.inputId);
-      }
-      return this.dropdown.before($input);
-    };
-
-    GitLabDropdown.prototype.selectRowAtIndex = function(index) {
-      var $el, selector;
-      // If we pass an option index
-      if (typeof index !== "undefined") {
-        selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
-      } else {
-        selector = ".dropdown-content .is-focused";
+    if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+      return;
+    }
+
+    if (el.hasClass(ACTIVE_CLASS)) {
+      isMarking = false;
+      el.removeClass(ACTIVE_CLASS);
+      if (field && field.length) {
+        this.clearField(field, isInput);
+      }
+    } else if (el.hasClass(INDETERMINATE_CLASS)) {
+      isMarking = true;
+      el.addClass(ACTIVE_CLASS);
+      el.removeClass(INDETERMINATE_CLASS);
+      if (field && field.length && value == null) {
+        this.clearField(field, isInput);
+      }
+      if ((!field || !field.length) && fieldName) {
+        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) {
+          this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
+        }
       }
-      if (this.dropdown.find(".dropdown-toggle-page").length) {
-        selector = ".dropdown-page-one " + selector;
+      if (field && field.length && value == null) {
+        this.clearField(field, isInput);
       }
-      // simulate a click on the first link
-      $el = $(selector, this.dropdown);
-      if ($el.length) {
-        var href = $el.attr('href');
-        if (href && href !== '#') {
-          gl.utils.visitUrl(href);
-        } else {
-          $el.first().trigger('click');
+      // Toggle active class for the tick mark
+      el.addClass(ACTIVE_CLASS);
+      if (value != null) {
+        if ((!field || !field.length) && fieldName) {
+          this.addInput(fieldName, value, selectedObject);
+        } else if (field && field.length) {
+          field.val(value).trigger('change');
         }
       }
-    };
+    }
 
-    GitLabDropdown.prototype.addArrowKeyEvent = function() {
-      var $input, ARROW_KEY_CODES, selector;
-      ARROW_KEY_CODES = [38, 40];
-      $input = this.dropdown.find(".dropdown-input-field");
-      selector = SELECTABLE_CLASSES;
-      if (this.dropdown.find(".dropdown-toggle-page").length) {
-        selector = ".dropdown-page-one " + selector;
+    return [selectedObject, isMarking];
+  };
+
+  GitLabDropdown.prototype.focusTextInput = function() {
+    if (this.options.filterable) { this.filterInput.focus(); }
+  };
+
+  GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+    var $input;
+    // Create hidden input for form
+    $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
+    if (this.options.inputId != null) {
+      $input.attr('id', this.options.inputId);
+    }
+    return this.dropdown.before($input);
+  };
+
+  GitLabDropdown.prototype.selectRowAtIndex = function(index) {
+    var $el, selector;
+    // If we pass an option index
+    if (typeof index !== "undefined") {
+      selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+    } else {
+      selector = ".dropdown-content .is-focused";
+    }
+    if (this.dropdown.find(".dropdown-toggle-page").length) {
+      selector = ".dropdown-page-one " + selector;
+    }
+    // simulate a click on the first link
+    $el = $(selector, this.dropdown);
+    if ($el.length) {
+      var href = $el.attr('href');
+      if (href && href !== '#') {
+        gl.utils.visitUrl(href);
+      } else {
+        $el.first().trigger('click');
       }
-      return $('body').on('keydown', (function(_this) {
-        return function(e) {
-          var $listItems, PREV_INDEX, currentKeyCode;
-          currentKeyCode = e.which;
-          if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) {
-            e.preventDefault();
-            e.stopImmediatePropagation();
-            PREV_INDEX = currentIndex;
-            $listItems = $(selector, _this.dropdown);
-            // if @options.filterable
-            //   $input.blur()
-            if (currentKeyCode === 40) {
-              // Move down
-              if (currentIndex < ($listItems.length - 1)) {
-                currentIndex += 1;
-              }
-            } else if (currentKeyCode === 38) {
-              // Move up
-              if (currentIndex > 0) {
-                currentIndex -= 1;
-              }
+    }
+  };
+
+  GitLabDropdown.prototype.addArrowKeyEvent = function() {
+    var $input, ARROW_KEY_CODES, selector;
+    ARROW_KEY_CODES = [38, 40];
+    $input = this.dropdown.find(".dropdown-input-field");
+    selector = SELECTABLE_CLASSES;
+    if (this.dropdown.find(".dropdown-toggle-page").length) {
+      selector = ".dropdown-page-one " + selector;
+    }
+    return $('body').on('keydown', (function(_this) {
+      return function(e) {
+        var $listItems, PREV_INDEX, currentKeyCode;
+        currentKeyCode = e.which;
+        if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+          e.preventDefault();
+          e.stopImmediatePropagation();
+          PREV_INDEX = currentIndex;
+          $listItems = $(selector, _this.dropdown);
+          // if @options.filterable
+          //   $input.blur()
+          if (currentKeyCode === 40) {
+            // Move down
+            if (currentIndex < ($listItems.length - 1)) {
+              currentIndex += 1;
             }
-            if (currentIndex !== PREV_INDEX) {
-              _this.highlightRowAtIndex($listItems, currentIndex);
+          } else if (currentKeyCode === 38) {
+            // Move up
+            if (currentIndex > 0) {
+              currentIndex -= 1;
             }
-            return false;
           }
-          if (currentKeyCode === 13 && currentIndex !== -1) {
-            e.preventDefault();
-            _this.selectRowAtIndex();
+          if (currentIndex !== PREV_INDEX) {
+            _this.highlightRowAtIndex($listItems, currentIndex);
           }
-        };
-      })(this));
-    };
-
-    GitLabDropdown.prototype.removeArrayKeyEvent = function() {
-      return $('body').off('keydown');
-    };
-
-    GitLabDropdown.prototype.resetRows = function resetRows() {
-      currentIndex = -1;
-      $('.is-focused', this.dropdown).removeClass('is-focused');
-    };
-
-    GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
-      var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
-      // Remove the class for the previously focused row
-      $('.is-focused', this.dropdown).removeClass('is-focused');
-      // Update the class for the row at the specific index
-      $listItem = $listItems.eq(index);
-      $listItem.find('a:first-child').addClass("is-focused");
-      // Dropdown content scroll area
-      $dropdownContent = $listItem.closest('.dropdown-content');
-      dropdownScrollTop = $dropdownContent.scrollTop();
-      dropdownContentHeight = $dropdownContent.outerHeight();
-      dropdownContentTop = $dropdownContent.prop('offsetTop');
-      dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
-      // Get the offset bottom of the list item
-      listItemHeight = $listItem.outerHeight();
-      listItemTop = $listItem.prop('offsetTop');
-      listItemBottom = listItemTop + listItemHeight;
-      if (!index) {
-        // Scroll the dropdown content to the top
-        $dropdownContent.scrollTop(0);
-      } else if (index === ($listItems.length - 1)) {
-        // Scroll the dropdown content to the bottom
-        $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
-      } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
-        // Scroll the dropdown content down
-        $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
-      } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
-        // Scroll the dropdown content up
-        return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
-      }
-    };
+          return false;
+        }
+        if (currentKeyCode === 13 && currentIndex !== -1) {
+          e.preventDefault();
+          _this.selectRowAtIndex();
+        }
+      };
+    })(this));
+  };
 
-    GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
-      if (selected == null) {
-        selected = null;
-      }
-      if (el == null) {
-        el = null;
-      }
-      if (instance == null) {
-        instance = null;
-      }
-      return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
-    };
+  GitLabDropdown.prototype.removeArrayKeyEvent = function() {
+    return $('body').off('keydown');
+  };
 
-    GitLabDropdown.prototype.clearField = function(field, isInput) {
-      return isInput ? field.val('') : field.remove();
-    };
+  GitLabDropdown.prototype.resetRows = function resetRows() {
+    currentIndex = -1;
+    $('.is-focused', this.dropdown).removeClass('is-focused');
+  };
 
-    return GitLabDropdown;
-  })();
+  GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
+    var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+    // Remove the class for the previously focused row
+    $('.is-focused', this.dropdown).removeClass('is-focused');
+    // Update the class for the row at the specific index
+    $listItem = $listItems.eq(index);
+    $listItem.find('a:first-child').addClass("is-focused");
+    // Dropdown content scroll area
+    $dropdownContent = $listItem.closest('.dropdown-content');
+    dropdownScrollTop = $dropdownContent.scrollTop();
+    dropdownContentHeight = $dropdownContent.outerHeight();
+    dropdownContentTop = $dropdownContent.prop('offsetTop');
+    dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+    // Get the offset bottom of the list item
+    listItemHeight = $listItem.outerHeight();
+    listItemTop = $listItem.prop('offsetTop');
+    listItemBottom = listItemTop + listItemHeight;
+    if (!index) {
+      // Scroll the dropdown content to the top
+      $dropdownContent.scrollTop(0);
+    } else if (index === ($listItems.length - 1)) {
+      // Scroll the dropdown content to the bottom
+      $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+    } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+      // Scroll the dropdown content down
+      $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+    } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+      // Scroll the dropdown content up
+      return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
+    }
+  };
 
-  $.fn.glDropdown = function(opts) {
-    return this.each(function() {
-      if (!$.data(this, 'glDropdown')) {
-        return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
-      }
-    });
+  GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
+    if (selected == null) {
+      selected = null;
+    }
+    if (el == null) {
+      el = null;
+    }
+    if (instance == null) {
+      instance = null;
+    }
+    return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
+  };
+
+  GitLabDropdown.prototype.clearField = function(field, isInput) {
+    return isInput ? field.val('') : field.remove();
   };
-}).call(window);
+
+  return GitLabDropdown;
+})();
+
+$.fn.glDropdown = function(opts) {
+  return this.each(function() {
+    if (!$.data(this, 'glDropdown')) {
+      return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
+    }
+  });
+};
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
new file mode 100644
index 0000000000000000000000000000000000000000..76de249ac3bb0122b527e84af35aea8a80f3cd4c
--- /dev/null
+++ b/app/assets/javascripts/gl_field_error.js
@@ -0,0 +1,162 @@
+/**
+ * This class overrides the browser's validation error bubbles, displaying custom
+ * error messages for invalid fields instead. To begin validating any form, add the
+ * class `gl-show-field-errors` to the form element, and ensure error messages are
+ * declared in each inputs' `title` attribute. If no title is declared for an invalid
+ * field the user attempts to submit, "This field is required." will be shown by default.
+ *
+ * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
+ *
+ * Set a custom error anchor for error message to be injected after with the
+ * class `gl-field-error-anchor`
+ *
+ * Examples:
+ *
+ * Basic:
+ *
+ * <form class='gl-show-field-errors'>
+ *  <input type='text' name='username' title='Username is required.'/>
+ * </form>
+ *
+ * Ignore specific inputs (e.g. UsernameValidator):
+ *
+ * <form class='gl-show-field-errors'>
+ *   <div class="form-group>
+ *     <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
+ *   </div>
+ *   <div class="form-group">
+ *      <input type='text' name='username' title='Username is required.'/>
+ *    </div>
+ * </form>
+ *
+ * Custom Error Anchor (allows error message to be injected after specified element):
+ *
+ * <form class='gl-show-field-errors'>
+ *  <div class="form-group gl-field-error-anchor">
+ *    <input type='text' name='username' title='Username is required.'/>
+ *    // Error message typically injected here
+ *  </div>
+ *  // Error message now injected here
+ * </form>
+ *
+ */
+
+/**
+ * Regex Patterns in use:
+ *
+ * Only alphanumeric: : "[a-zA-Z0-9]+"
+ * No special characters : "[a-zA-Z0-9-_]+",
+ *
+ */
+
+const errorMessageClass = 'gl-field-error';
+const inputErrorClass = 'gl-field-error-outline';
+const errorAnchorSelector = '.gl-field-error-anchor';
+const ignoreInputSelector = '.gl-field-error-ignore';
+
+class GlFieldError {
+  constructor({ input, formErrors }) {
+    this.inputElement = $(input);
+    this.inputDomElement = this.inputElement.get(0);
+    this.form = formErrors;
+    this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+    this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+
+    this.state = {
+      valid: false,
+      empty: true,
+    };
+
+    this.initFieldValidation();
+  }
+
+  initFieldValidation() {
+    const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
+    const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
+
+    // hidden when injected into DOM
+    errorAnchor.after(this.fieldErrorElement);
+    this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
+    this.scopedSiblings = this.safelySelectSiblings();
+  }
+
+  safelySelectSiblings() {
+    // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
+    const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
+    const parentContainer = this.inputElement.parent('.form-group');
+
+    // Only select siblings when they're scoped within a form-group with one input
+    const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
+
+    return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
+  }
+
+  renderValidity() {
+    this.renderClear();
+
+    if (this.state.valid) {
+      this.renderValid();
+    } else if (this.state.empty) {
+      this.renderEmpty();
+    } else if (!this.state.valid) {
+      this.renderInvalid();
+    }
+  }
+
+  handleInvalidSubmit(event) {
+    event.preventDefault();
+    const currentValue = this.accessCurrentValue();
+    this.state.valid = false;
+    this.state.empty = currentValue === '';
+
+    this.renderValidity();
+    this.form.focusOnFirstInvalid.apply(this.form);
+    // For UX, wait til after first invalid submission to check each keyup
+    this.inputElement.off('keyup.fieldValidator')
+      .on('keyup.fieldValidator', this.updateValidity.bind(this));
+  }
+
+  /* Get or set current input value */
+  accessCurrentValue(newVal) {
+    return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
+  }
+
+  getInputValidity() {
+    return this.inputDomElement.validity.valid;
+  }
+
+  updateValidity() {
+    const inputVal = this.accessCurrentValue();
+    this.state.empty = !inputVal.length;
+    this.state.valid = this.getInputValidity();
+    this.renderValidity();
+  }
+
+  renderValid() {
+    return this.renderClear();
+  }
+
+  renderEmpty() {
+    return this.renderInvalid();
+  }
+
+  renderInvalid() {
+    this.inputElement.addClass(inputErrorClass);
+    this.scopedSiblings.hide();
+    return this.fieldErrorElement.show();
+  }
+
+  renderClear() {
+    const inputVal = this.accessCurrentValue();
+    if (!inputVal.split(' ').length) {
+      const trimmedInput = inputVal.trim();
+      this.accessCurrentValue(trimmedInput);
+    }
+    this.inputElement.removeClass(inputErrorClass);
+    this.scopedSiblings.hide();
+    this.fieldErrorElement.hide();
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.GlFieldError = GlFieldError;
diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6
deleted file mode 100644
index f7cbecc0385e9c05779da3a8e7410cbd464b08a5..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/gl_field_error.js.es6
+++ /dev/null
@@ -1,164 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
-  /*
-   * This class overrides the browser's validation error bubbles, displaying custom
-   * error messages for invalid fields instead. To begin validating any form, add the
-   * class `gl-show-field-errors` to the form element, and ensure error messages are
-   * declared in each inputs' `title` attribute. If no title is declared for an invalid
-   * field the user attempts to submit, "This field is required." will be shown by default.
-   *
-   * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
-   *
-   * Set a custom error anchor for error message to be injected after with the
-   * class `gl-field-error-anchor`
-   *
-   * Examples:
-   *
-   * Basic:
-   *
-   * <form class='gl-show-field-errors'>
-   *  <input type='text' name='username' title='Username is required.'/>
-   * </form>
-   *
-   * Ignore specific inputs (e.g. UsernameValidator):
-   *
-   * <form class='gl-show-field-errors'>
-   *   <div class="form-group>
-   *     <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
-   *   </div>
-   *   <div class="form-group">
-   *      <input type='text' name='username' title='Username is required.'/>
-   *    </div>
-   * </form>
-   *
-   * Custom Error Anchor (allows error message to be injected after specified element):
-   *
-   * <form class='gl-show-field-errors'>
-   *  <div class="form-group gl-field-error-anchor">
-   *    <input type='text' name='username' title='Username is required.'/>
-   *    // Error message typically injected here
-   *  </div>
-   *  // Error message now injected here
-   * </form>
-   *
-    * */
-
-  /*
-    * Regex Patterns in use:
-    *
-    * Only alphanumeric: : "[a-zA-Z0-9]+"
-    * No special characters : "[a-zA-Z0-9-_]+",
-    *
-    * */
-
-  const errorMessageClass = 'gl-field-error';
-  const inputErrorClass = 'gl-field-error-outline';
-  const errorAnchorSelector = '.gl-field-error-anchor';
-  const ignoreInputSelector = '.gl-field-error-ignore';
-
-  class GlFieldError {
-    constructor({ input, formErrors }) {
-      this.inputElement = $(input);
-      this.inputDomElement = this.inputElement.get(0);
-      this.form = formErrors;
-      this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
-      this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
-
-      this.state = {
-        valid: false,
-        empty: true,
-      };
-
-      this.initFieldValidation();
-    }
-
-    initFieldValidation() {
-      const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
-      const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
-
-      // hidden when injected into DOM
-      errorAnchor.after(this.fieldErrorElement);
-      this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
-      this.scopedSiblings = this.safelySelectSiblings();
-    }
-
-    safelySelectSiblings() {
-      // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
-      const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
-      const parentContainer = this.inputElement.parent('.form-group');
-
-      // Only select siblings when they're scoped within a form-group with one input
-      const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
-
-      return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
-    }
-
-    renderValidity() {
-      this.renderClear();
-
-      if (this.state.valid) {
-        this.renderValid();
-      } else if (this.state.empty) {
-        this.renderEmpty();
-      } else if (!this.state.valid) {
-        this.renderInvalid();
-      }
-    }
-
-    handleInvalidSubmit(event) {
-      event.preventDefault();
-      const currentValue = this.accessCurrentValue();
-      this.state.valid = false;
-      this.state.empty = currentValue === '';
-
-      this.renderValidity();
-      this.form.focusOnFirstInvalid.apply(this.form);
-      // For UX, wait til after first invalid submission to check each keyup
-      this.inputElement.off('keyup.fieldValidator')
-        .on('keyup.fieldValidator', this.updateValidity.bind(this));
-    }
-
-    /* Get or set current input value */
-    accessCurrentValue(newVal) {
-      return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
-    }
-
-    getInputValidity() {
-      return this.inputDomElement.validity.valid;
-    }
-
-    updateValidity() {
-      const inputVal = this.accessCurrentValue();
-      this.state.empty = !inputVal.length;
-      this.state.valid = this.getInputValidity();
-      this.renderValidity();
-    }
-
-    renderValid() {
-      return this.renderClear();
-    }
-
-    renderEmpty() {
-      return this.renderInvalid();
-    }
-
-    renderInvalid() {
-      this.inputElement.addClass(inputErrorClass);
-      this.scopedSiblings.hide();
-      return this.fieldErrorElement.show();
-    }
-
-    renderClear() {
-      const inputVal = this.accessCurrentValue();
-      if (!inputVal.split(' ').length) {
-        const trimmedInput = inputVal.trim();
-        this.accessCurrentValue(trimmedInput);
-      }
-      this.inputElement.removeClass(inputErrorClass);
-      this.scopedSiblings.hide();
-      this.fieldErrorElement.hide();
-    }
-  }
-
-  global.GlFieldError = GlFieldError;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
new file mode 100644
index 0000000000000000000000000000000000000000..636258ec555656e44b48d8ae8dea20481b2df283
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -0,0 +1,47 @@
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
+
+require('./gl_field_error');
+
+const customValidationFlag = 'gl-field-error-ignore';
+
+class GlFieldErrors {
+  constructor(form) {
+    this.form = $(form);
+    this.state = {
+      inputs: [],
+      valid: false
+    };
+    this.initValidators();
+  }
+
+  initValidators () {
+    // register selectors here as needed
+    const validateSelectors = [':text', ':password', '[type=email]']
+      .map((selector) => `input${selector}`).join(',');
+
+    this.state.inputs = this.form.find(validateSelectors).toArray()
+      .filter((input) => !input.classList.contains(customValidationFlag))
+      .map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
+
+    this.form.on('submit', this.catchInvalidFormSubmit);
+  }
+
+  /* Neccessary to prevent intercept and override invalid form submit
+   * because Safari & iOS quietly allow form submission when form is invalid
+   * and prevents disabling of invalid submit button by application.js */
+
+  catchInvalidFormSubmit (event) {
+    if (!event.currentTarget.checkValidity()) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  }
+
+  focusOnFirstInvalid () {
+    const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+    firstInvalid.inputElement.focus();
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.GlFieldErrors = GlFieldErrors;
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
deleted file mode 100644
index e9add115429a6908b16d5173a0aa47a308e9e687..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-
-require('./gl_field_error');
-
-((global) => {
-  const customValidationFlag = 'gl-field-error-ignore';
-
-  class GlFieldErrors {
-    constructor(form) {
-      this.form = $(form);
-      this.state = {
-        inputs: [],
-        valid: false
-      };
-      this.initValidators();
-    }
-
-    initValidators () {
-      // register selectors here as needed
-      const validateSelectors = [':text', ':password', '[type=email]']
-        .map((selector) => `input${selector}`).join(',');
-
-      this.state.inputs = this.form.find(validateSelectors).toArray()
-        .filter((input) => !input.classList.contains(customValidationFlag))
-        .map((input) => new global.GlFieldError({ input, formErrors: this }));
-
-      this.form.on('submit', this.catchInvalidFormSubmit);
-    }
-
-    /* Neccessary to prevent intercept and override invalid form submit
-     * because Safari & iOS quietly allow form submission when form is invalid
-     * and prevents disabling of invalid submit button by application.js */
-
-    catchInvalidFormSubmit (event) {
-      if (!event.currentTarget.checkValidity()) {
-        event.preventDefault();
-        event.stopPropagation();
-      }
-    }
-
-    focusOnFirstInvalid () {
-      const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
-      firstInvalid.inputElement.focus();
-    }
-  }
-
-  global.GlFieldErrors = GlFieldErrors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
new file mode 100644
index 0000000000000000000000000000000000000000..e7c98e1658152df0e6debfaf39c798c228104f39
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js
@@ -0,0 +1,90 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
+window.gl = window.gl || {};
+
+function GLForm(form) {
+  this.form = form;
+  this.textarea = this.form.find('textarea.js-gfm-input');
+  // Before we start, we should clean up any previous data for this form
+  this.destroy();
+  // Setup the form
+  this.setupForm();
+  this.form.data('gl-form', this);
+}
+
+GLForm.prototype.destroy = function() {
+  // Clean form listeners
+  this.clearEventListeners();
+  return this.form.data('gl-form', null);
+};
+
+GLForm.prototype.setupForm = function() {
+  var isNewForm;
+  isNewForm = this.form.is(':not(.gfm-form)');
+  this.form.removeClass('js-new-note-form');
+  if (isNewForm) {
+    this.form.find('.div-dropzone').remove();
+    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'));
+    gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+    new DropzoneInput(this.form);
+    autosize(this.textarea);
+    // form and textarea event listeners
+    this.addEventListeners();
+  }
+  gl.text.init(this.form);
+  // hide discard button
+  this.form.find('.js-note-discard').hide();
+  this.form.show();
+  if (this.isAutosizeable) this.setupAutosize();
+};
+
+GLForm.prototype.setupAutosize = function () {
+  this.textarea.off('autosize:resized')
+    .on('autosize:resized', this.setHeightData.bind(this));
+
+  this.textarea.off('mouseup.autosize')
+    .on('mouseup.autosize', this.destroyAutosize.bind(this));
+
+  setTimeout(() => {
+    autosize(this.textarea);
+    this.textarea.css('resize', 'vertical');
+  }, 0);
+};
+
+GLForm.prototype.setHeightData = function () {
+  this.textarea.data('height', this.textarea.outerHeight());
+};
+
+GLForm.prototype.destroyAutosize = function () {
+  const outerHeight = this.textarea.outerHeight();
+
+  if (this.textarea.data('height') === outerHeight) return;
+
+  autosize.destroy(this.textarea);
+
+  this.textarea.data('height', outerHeight);
+  this.textarea.outerHeight(outerHeight);
+  this.textarea.css('max-height', window.outerHeight);
+};
+
+GLForm.prototype.clearEventListeners = function() {
+  this.textarea.off('focus');
+  this.textarea.off('blur');
+  return gl.text.removeListeners(this.form);
+};
+
+GLForm.prototype.addEventListeners = function() {
+  this.textarea.on('focus', function() {
+    return $(this).closest('.md-area').addClass('is-focused');
+  });
+  return this.textarea.on('blur', function() {
+    return $(this).closest('.md-area').removeClass('is-focused');
+  });
+};
+
+window.gl.GLForm = GLForm;
diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6
deleted file mode 100644
index 0b446ff364a4fba41ca403a2ef509d3c2d0d27bd..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/gl_form.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
-/* global GitLab */
-/* global DropzoneInput */
-/* global autosize */
-
-(() => {
-  const global = window.gl || (window.gl = {});
-
-  function GLForm(form) {
-    this.form = form;
-    this.textarea = this.form.find('textarea.js-gfm-input');
-    // Before we start, we should clean up any previous data for this form
-    this.destroy();
-    // Setup the form
-    this.setupForm();
-    this.form.data('gl-form', this);
-  }
-
-  GLForm.prototype.destroy = function() {
-    // Clean form listeners
-    this.clearEventListeners();
-    return this.form.data('gl-form', null);
-  };
-
-  GLForm.prototype.setupForm = function() {
-    var isNewForm;
-    isNewForm = this.form.is(':not(.gfm-form)');
-    this.form.removeClass('js-new-note-form');
-    if (isNewForm) {
-      this.form.find('.div-dropzone').remove();
-      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'));
-      gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
-      new DropzoneInput(this.form);
-      autosize(this.textarea);
-      // form and textarea event listeners
-      this.addEventListeners();
-    }
-    gl.text.init(this.form);
-    // hide discard button
-    this.form.find('.js-note-discard').hide();
-    this.form.show();
-    if (this.isAutosizeable) this.setupAutosize();
-  };
-
-  GLForm.prototype.setupAutosize = function () {
-    this.textarea.off('autosize:resized')
-      .on('autosize:resized', this.setHeightData.bind(this));
-
-    this.textarea.off('mouseup.autosize')
-      .on('mouseup.autosize', this.destroyAutosize.bind(this));
-
-    setTimeout(() => {
-      autosize(this.textarea);
-      this.textarea.css('resize', 'vertical');
-    }, 0);
-  };
-
-  GLForm.prototype.setHeightData = function () {
-    this.textarea.data('height', this.textarea.outerHeight());
-  };
-
-  GLForm.prototype.destroyAutosize = function () {
-    const outerHeight = this.textarea.outerHeight();
-
-    if (this.textarea.data('height') === outerHeight) return;
-
-    autosize.destroy(this.textarea);
-
-    this.textarea.data('height', outerHeight);
-    this.textarea.outerHeight(outerHeight);
-    this.textarea.css('max-height', window.outerHeight);
-  };
-
-  GLForm.prototype.clearEventListeners = function() {
-    this.textarea.off('focus');
-    this.textarea.off('blur');
-    return gl.text.removeListeners(this.form);
-  };
-
-  GLForm.prototype.addEventListeners = function() {
-    this.textarea.on('focus', function() {
-      return $(this).closest('.md-area').addClass('is-focused');
-    });
-    return this.textarea.on('blur', function() {
-      return $(this).closest('.md-area').removeClass('is-focused');
-    });
-  };
-
-  global.GLForm = GLForm;
-})();
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 4f7777aa5bc68fa75ecb8b5435e34fc17a7017fc..a433c7ba8f0ca6f90af5b3fc2427253818cda81e 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,3 +1,6 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
+import Chart from 'vendor/Chart';
+import ContributorsStatGraph from './stat_graph_contributors';
+
+// export to global scope
+window.Chart = Chart;
+window.ContributorsStatGraph = ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
deleted file mode 100644
index 75a53aae33cac91ca53454fd750320e2b59f4434..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
-(function() {
-  this.StatGraph = (function() {
-    function StatGraph() {}
-
-    StatGraph.log = {};
-
-    StatGraph.get_log = function() {
-      return this.log;
-    };
-
-    StatGraph.set_log = function(data) {
-      return this.log = data;
-    };
-
-    return StatGraph;
-  })();
-}).call(window);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index bbfb467ad5079a722edd65135e58a71d34c74ec6..c6be4c9e8feb184d93a69229a18cf350e2efe539 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,116 +1,111 @@
-/* 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 */
-/* global ContributorsGraph */
-/* global ContributorsAuthorGraph */
-/* global ContributorsMasterGraph */
-/* global ContributorsStatGraphUtil */
-/* global d3 */
+/* 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, no-shadow */
 
-window.d3 = require('d3');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
+import ContributorsStatGraphUtil from './stat_graph_contributors_util';
 
-(function() {
-  this.ContributorsStatGraph = (function() {
-    function ContributorsStatGraph() {}
+export default (function() {
+  function ContributorsStatGraph() {}
 
-    ContributorsStatGraph.prototype.init = function(log) {
-      var author_commits, total_commits;
-      this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
-      this.set_current_field("commits");
-      total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
-      author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
-      this.add_master_graph(total_commits);
-      this.add_authors_graph(author_commits);
-      return this.change_date_header();
-    };
+  ContributorsStatGraph.prototype.init = function(log) {
+    var author_commits, total_commits;
+    this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
+    this.set_current_field("commits");
+    total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+    author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
+    this.add_master_graph(total_commits);
+    this.add_authors_graph(author_commits);
+    return this.change_date_header();
+  };
 
-    ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
-      this.master_graph = new ContributorsMasterGraph(total_data);
-      return this.master_graph.draw();
-    };
+  ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
+    this.master_graph = new ContributorsMasterGraph(total_data);
+    return this.master_graph.draw();
+  };
 
-    ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
-      var limited_author_data;
-      this.authors = [];
-      limited_author_data = author_data.slice(0, 100);
-      return _.each(limited_author_data, (function(_this) {
-        return function(d) {
-          var author_graph, author_header;
-          author_header = _this.create_author_header(d);
-          $(".contributors-list").append(author_header);
-          _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
-          return author_graph.draw();
-        };
-      })(this));
-    };
+  ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
+    var limited_author_data;
+    this.authors = [];
+    limited_author_data = author_data.slice(0, 100);
+    return _.each(limited_author_data, (function(_this) {
+      return function(d) {
+        var author_graph, author_header;
+        author_header = _this.create_author_header(d);
+        $(".contributors-list").append(author_header);
+        _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+        return author_graph.draw();
+      };
+    })(this));
+  };
 
-    ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
-      var commits;
-      commits = $('<span/>', {
-        "class": 'graph-author-commits-count'
-      });
-      commits.text(author.commits + " commits");
-      return $('<span/>').append(commits);
-    };
+  ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
+    var commits;
+    commits = $('<span/>', {
+      "class": 'graph-author-commits-count'
+    });
+    commits.text(author.commits + " commits");
+    return $('<span/>').append(commits);
+  };
 
-    ContributorsStatGraph.prototype.create_author_header = function(author) {
-      var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
-      list_item = $('<li/>', {
-        "class": 'person',
-        style: 'display: block;'
-      });
-      author_name = $('<h4>' + author.author_name + '</h4>');
-      author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
-      author_commit_info_span = $('<span/>', {
-        "class": 'commits'
-      });
-      author_commit_info = this.format_author_commit_info(author);
-      author_commit_info_span.html(author_commit_info);
-      list_item.append(author_name);
-      list_item.append(author_email);
-      list_item.append(author_commit_info_span);
-      return list_item;
-    };
+  ContributorsStatGraph.prototype.create_author_header = function(author) {
+    var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
+    list_item = $('<li/>', {
+      "class": 'person',
+      style: 'display: block;'
+    });
+    author_name = $('<h4>' + author.author_name + '</h4>');
+    author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
+    author_commit_info_span = $('<span/>', {
+      "class": 'commits'
+    });
+    author_commit_info = this.format_author_commit_info(author);
+    author_commit_info_span.html(author_commit_info);
+    list_item.append(author_name);
+    list_item.append(author_email);
+    list_item.append(author_commit_info_span);
+    return list_item;
+  };
 
-    ContributorsStatGraph.prototype.redraw_master = function() {
-      var total_data;
-      total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
-      this.master_graph.set_data(total_data);
-      return this.master_graph.redraw();
-    };
+  ContributorsStatGraph.prototype.redraw_master = function() {
+    var total_data;
+    total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+    this.master_graph.set_data(total_data);
+    return this.master_graph.redraw();
+  };
 
-    ContributorsStatGraph.prototype.redraw_authors = function() {
-      var author_commits, x_domain;
-      $("ol").html("");
-      x_domain = ContributorsGraph.prototype.x_domain;
-      author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
-      return _.each(author_commits, (function(_this) {
-        return function(d) {
-          _this.redraw_author_commit_info(d);
-          $(_this.authors[d.author_name].list_item).appendTo("ol");
-          _this.authors[d.author_name].set_data(d.dates);
-          return _this.authors[d.author_name].redraw();
-        };
-      })(this));
-    };
+  ContributorsStatGraph.prototype.redraw_authors = function() {
+    var author_commits, x_domain;
+    $("ol").html("");
+    x_domain = ContributorsGraph.prototype.x_domain;
+    author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+    return _.each(author_commits, (function(_this) {
+      return function(d) {
+        _this.redraw_author_commit_info(d);
+        $(_this.authors[d.author_name].list_item).appendTo("ol");
+        _this.authors[d.author_name].set_data(d.dates);
+        return _this.authors[d.author_name].redraw();
+      };
+    })(this));
+  };
 
-    ContributorsStatGraph.prototype.set_current_field = function(field) {
-      return this.field = field;
-    };
+  ContributorsStatGraph.prototype.set_current_field = function(field) {
+    return this.field = field;
+  };
 
-    ContributorsStatGraph.prototype.change_date_header = function() {
-      var print, print_date_format, x_domain;
-      x_domain = ContributorsGraph.prototype.x_domain;
-      print_date_format = d3.time.format("%B %e %Y");
-      print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
-      return $("#date_header").text(print);
-    };
+  ContributorsStatGraph.prototype.change_date_header = function() {
+    var print, print_date_format, x_domain;
+    x_domain = ContributorsGraph.prototype.x_domain;
+    print_date_format = d3.time.format("%B %e %Y");
+    print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
+    return $("#date_header").text(print);
+  };
 
-    ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
-      var author_commit_info, author_list_item;
-      author_list_item = $(this.authors[author.author_name].list_item);
-      author_commit_info = this.format_author_commit_info(author);
-      return author_list_item.find("span").html(author_commit_info);
-    };
+  ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
+    var author_commit_info, author_list_item;
+    author_list_item = $(this.authors[author.author_name].list_item);
+    author_commit_info = this.format_author_commit_info(author);
+    return author_list_item.find("span").html(author_commit_info);
+  };
 
-    return ContributorsStatGraph;
-  })();
-}).call(window);
+  return ContributorsStatGraph;
+})();
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 228771da4eed5596b0d2020c50f46d0535bb1938..521bc77db662deb06e76d2f45382af19c2791a47 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,276 +1,272 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, 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, newline-per-chained-call, no-else-return */
-/* global d3 */
-/* global ContributorsGraph */
-
-window.d3 = require('d3');
-
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
-    extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
-    hasProp = {}.hasOwnProperty;
-
-  this.ContributorsGraph = (function() {
-    function ContributorsGraph() {}
-
-    ContributorsGraph.prototype.MARGIN = {
-      top: 20,
-      right: 20,
-      bottom: 30,
-      left: 50
-    };
-
-    ContributorsGraph.prototype.x_domain = null;
-
-    ContributorsGraph.prototype.y_domain = null;
-
-    ContributorsGraph.prototype.dates = [];
-
-    ContributorsGraph.set_x_domain = function(data) {
-      return ContributorsGraph.prototype.x_domain = data;
-    };
-
-    ContributorsGraph.set_y_domain = function(data) {
-      return ContributorsGraph.prototype.y_domain = [
-        0, d3.max(data, function(d) {
-          return d.commits = d.commits || d.additions || d.deletions;
-        })
-      ];
-    };
-
-    ContributorsGraph.init_x_domain = function(data) {
-      return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
-        return d.date;
-      });
-    };
-
-    ContributorsGraph.init_y_domain = function(data) {
-      return ContributorsGraph.prototype.y_domain = [
-        0, d3.max(data, function(d) {
-          return d.commits = d.commits || d.additions || d.deletions;
-        })
-      ];
-    };
-
-    ContributorsGraph.init_domain = function(data) {
-      ContributorsGraph.init_x_domain(data);
-      return ContributorsGraph.init_y_domain(data);
-    };
-
-    ContributorsGraph.set_dates = function(data) {
-      return ContributorsGraph.prototype.dates = data;
-    };
-
-    ContributorsGraph.prototype.set_x_domain = function() {
-      return this.x.domain(this.x_domain);
-    };
-
-    ContributorsGraph.prototype.set_y_domain = function() {
-      return this.y.domain(this.y_domain);
-    };
-
-    ContributorsGraph.prototype.set_domain = function() {
-      this.set_x_domain();
-      return this.set_y_domain();
-    };
-
-    ContributorsGraph.prototype.create_scale = function(width, height) {
-      this.x = d3.time.scale().range([0, width]).clamp(true);
-      return this.y = d3.scale.linear().range([height, 0]).nice();
-    };
-
-    ContributorsGraph.prototype.draw_x_axis = function() {
-      return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
-    };
-
-    ContributorsGraph.prototype.draw_y_axis = function() {
-      return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
-    };
-
-    ContributorsGraph.prototype.set_data = function(data) {
-      return this.data = data;
-    };
-
-    return ContributorsGraph;
-  })();
-
-  this.ContributorsMasterGraph = (function(superClass) {
-    extend(ContributorsMasterGraph, superClass);
-
-    function ContributorsMasterGraph(data1) {
-      this.data = data1;
-      this.update_content = bind(this.update_content, this);
-      this.width = $('.content').width() - 70;
-      this.height = 200;
-      this.x = null;
-      this.y = null;
-      this.x_axis = null;
-      this.y_axis = null;
-      this.area = null;
-      this.svg = null;
-      this.brush = null;
-      this.x_max_domain = null;
+/* eslint-disable func-names, space-before-function-paren, no-var, 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, newline-per-chained-call, no-else-return, no-shadow */
+
+import d3 from 'd3';
+
+const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+const hasProp = {}.hasOwnProperty;
+
+export const ContributorsGraph = (function() {
+  function ContributorsGraph() {}
+
+  ContributorsGraph.prototype.MARGIN = {
+    top: 20,
+    right: 20,
+    bottom: 30,
+    left: 50
+  };
+
+  ContributorsGraph.prototype.x_domain = null;
+
+  ContributorsGraph.prototype.y_domain = null;
+
+  ContributorsGraph.prototype.dates = [];
+
+  ContributorsGraph.set_x_domain = function(data) {
+    return ContributorsGraph.prototype.x_domain = data;
+  };
+
+  ContributorsGraph.set_y_domain = function(data) {
+    return ContributorsGraph.prototype.y_domain = [
+      0, d3.max(data, function(d) {
+        return d.commits = d.commits || d.additions || d.deletions;
+      })
+    ];
+  };
+
+  ContributorsGraph.init_x_domain = function(data) {
+    return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+      return d.date;
+    });
+  };
+
+  ContributorsGraph.init_y_domain = function(data) {
+    return ContributorsGraph.prototype.y_domain = [
+      0, d3.max(data, function(d) {
+        return d.commits = d.commits || d.additions || d.deletions;
+      })
+    ];
+  };
+
+  ContributorsGraph.init_domain = function(data) {
+    ContributorsGraph.init_x_domain(data);
+    return ContributorsGraph.init_y_domain(data);
+  };
+
+  ContributorsGraph.set_dates = function(data) {
+    return ContributorsGraph.prototype.dates = data;
+  };
+
+  ContributorsGraph.prototype.set_x_domain = function() {
+    return this.x.domain(this.x_domain);
+  };
+
+  ContributorsGraph.prototype.set_y_domain = function() {
+    return this.y.domain(this.y_domain);
+  };
+
+  ContributorsGraph.prototype.set_domain = function() {
+    this.set_x_domain();
+    return this.set_y_domain();
+  };
+
+  ContributorsGraph.prototype.create_scale = function(width, height) {
+    this.x = d3.time.scale().range([0, width]).clamp(true);
+    return this.y = d3.scale.linear().range([height, 0]).nice();
+  };
+
+  ContributorsGraph.prototype.draw_x_axis = function() {
+    return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+  };
+
+  ContributorsGraph.prototype.draw_y_axis = function() {
+    return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+  };
+
+  ContributorsGraph.prototype.set_data = function(data) {
+    return this.data = data;
+  };
+
+  return ContributorsGraph;
+})();
+
+export const ContributorsMasterGraph = (function(superClass) {
+  extend(ContributorsMasterGraph, superClass);
+
+  function ContributorsMasterGraph(data1) {
+    this.data = data1;
+    this.update_content = bind(this.update_content, this);
+    this.width = $('.content').width() - 70;
+    this.height = 200;
+    this.x = null;
+    this.y = null;
+    this.x_axis = null;
+    this.y_axis = null;
+    this.area = null;
+    this.svg = null;
+    this.brush = null;
+    this.x_max_domain = null;
+  }
+
+  ContributorsMasterGraph.prototype.process_dates = function(data) {
+    var dates;
+    dates = this.get_dates(data);
+    this.parse_dates(data);
+    return ContributorsGraph.set_dates(dates);
+  };
+
+  ContributorsMasterGraph.prototype.get_dates = function(data) {
+    return _.pluck(data, 'date');
+  };
+
+  ContributorsMasterGraph.prototype.parse_dates = function(data) {
+    var parseDate;
+    parseDate = d3.time.format("%Y-%m-%d").parse;
+    return data.forEach(function(d) {
+      return d.date = parseDate(d.date);
+    });
+  };
+
+  ContributorsMasterGraph.prototype.create_scale = function() {
+    return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
+  };
+
+  ContributorsMasterGraph.prototype.create_axes = function() {
+    this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
+    return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+  };
+
+  ContributorsMasterGraph.prototype.create_svg = function() {
+    return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+  };
+
+  ContributorsMasterGraph.prototype.create_area = function(x, y) {
+    return this.area = d3.svg.area().x(function(d) {
+      return x(d.date);
+    }).y0(this.height).y1(function(d) {
+      d.commits = d.commits || d.additions || d.deletions;
+      return y(d.commits);
+    }).interpolate("basis");
+  };
+
+  ContributorsMasterGraph.prototype.create_brush = function() {
+    return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+  };
+
+  ContributorsMasterGraph.prototype.draw_path = function(data) {
+    return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+  };
+
+  ContributorsMasterGraph.prototype.add_brush = function() {
+    return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+  };
+
+  ContributorsMasterGraph.prototype.update_content = function() {
+    ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+    return $("#brush_change").trigger('change');
+  };
+
+  ContributorsMasterGraph.prototype.draw = function() {
+    this.process_dates(this.data);
+    this.create_scale();
+    this.create_axes();
+    ContributorsGraph.init_domain(this.data);
+    this.x_max_domain = this.x_domain;
+    this.set_domain();
+    this.create_area(this.x, this.y);
+    this.create_svg();
+    this.create_brush();
+    this.draw_path(this.data);
+    this.draw_x_axis();
+    this.draw_y_axis();
+    return this.add_brush();
+  };
+
+  ContributorsMasterGraph.prototype.redraw = function() {
+    this.process_dates(this.data);
+    ContributorsGraph.set_y_domain(this.data);
+    this.set_y_domain();
+    this.svg.select("path").datum(this.data);
+    this.svg.select("path").attr("d", this.area);
+    return this.svg.select(".y.axis").call(this.y_axis);
+  };
+
+  return ContributorsMasterGraph;
+})(ContributorsGraph);
+
+export const ContributorsAuthorGraph = (function(superClass) {
+  extend(ContributorsAuthorGraph, superClass);
+
+  function ContributorsAuthorGraph(data1) {
+    this.data = data1;
+    // Don't split graph size in half for mobile devices.
+    if ($(window).width() < 768) {
+      this.width = $('.content').width() - 80;
+    } else {
+      this.width = ($('.content').width() / 2) - 100;
     }
-
-    ContributorsMasterGraph.prototype.process_dates = function(data) {
-      var dates;
-      dates = this.get_dates(data);
-      this.parse_dates(data);
-      return ContributorsGraph.set_dates(dates);
-    };
-
-    ContributorsMasterGraph.prototype.get_dates = function(data) {
-      return _.pluck(data, 'date');
-    };
-
-    ContributorsMasterGraph.prototype.parse_dates = function(data) {
+    this.height = 200;
+    this.x = null;
+    this.y = null;
+    this.x_axis = null;
+    this.y_axis = null;
+    this.area = null;
+    this.svg = null;
+    this.list_item = null;
+  }
+
+  ContributorsAuthorGraph.prototype.create_scale = function() {
+    return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
+  };
+
+  ContributorsAuthorGraph.prototype.create_axes = function() {
+    this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
+    return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+  };
+
+  ContributorsAuthorGraph.prototype.create_area = function(x, y) {
+    return this.area = d3.svg.area().x(function(d) {
       var parseDate;
       parseDate = d3.time.format("%Y-%m-%d").parse;
-      return data.forEach(function(d) {
-        return d.date = parseDate(d.date);
-      });
-    };
-
-    ContributorsMasterGraph.prototype.create_scale = function() {
-      return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
-    };
-
-    ContributorsMasterGraph.prototype.create_axes = function() {
-      this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
-      return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
-    };
-
-    ContributorsMasterGraph.prototype.create_svg = function() {
-      return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
-    };
-
-    ContributorsMasterGraph.prototype.create_area = function(x, y) {
-      return this.area = d3.svg.area().x(function(d) {
-        return x(d.date);
-      }).y0(this.height).y1(function(d) {
-        d.commits = d.commits || d.additions || d.deletions;
-        return y(d.commits);
-      }).interpolate("basis");
-    };
-
-    ContributorsMasterGraph.prototype.create_brush = function() {
-      return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
-    };
-
-    ContributorsMasterGraph.prototype.draw_path = function(data) {
-      return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
-    };
-
-    ContributorsMasterGraph.prototype.add_brush = function() {
-      return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
-    };
-
-    ContributorsMasterGraph.prototype.update_content = function() {
-      ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
-      return $("#brush_change").trigger('change');
-    };
-
-    ContributorsMasterGraph.prototype.draw = function() {
-      this.process_dates(this.data);
-      this.create_scale();
-      this.create_axes();
-      ContributorsGraph.init_domain(this.data);
-      this.x_max_domain = this.x_domain;
-      this.set_domain();
-      this.create_area(this.x, this.y);
-      this.create_svg();
-      this.create_brush();
-      this.draw_path(this.data);
-      this.draw_x_axis();
-      this.draw_y_axis();
-      return this.add_brush();
-    };
-
-    ContributorsMasterGraph.prototype.redraw = function() {
-      this.process_dates(this.data);
-      ContributorsGraph.set_y_domain(this.data);
-      this.set_y_domain();
-      this.svg.select("path").datum(this.data);
-      this.svg.select("path").attr("d", this.area);
-      return this.svg.select(".y.axis").call(this.y_axis);
-    };
-
-    return ContributorsMasterGraph;
-  })(ContributorsGraph);
-
-  this.ContributorsAuthorGraph = (function(superClass) {
-    extend(ContributorsAuthorGraph, superClass);
-
-    function ContributorsAuthorGraph(data1) {
-      this.data = data1;
-      // Don't split graph size in half for mobile devices.
-      if ($(window).width() < 768) {
-        this.width = $('.content').width() - 80;
-      } else {
-        this.width = ($('.content').width() / 2) - 100;
-      }
-      this.height = 200;
-      this.x = null;
-      this.y = null;
-      this.x_axis = null;
-      this.y_axis = null;
-      this.area = null;
-      this.svg = null;
-      this.list_item = null;
-    }
-
-    ContributorsAuthorGraph.prototype.create_scale = function() {
-      return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
-    };
-
-    ContributorsAuthorGraph.prototype.create_axes = function() {
-      this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
-      return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
-    };
-
-    ContributorsAuthorGraph.prototype.create_area = function(x, y) {
-      return this.area = d3.svg.area().x(function(d) {
-        var parseDate;
-        parseDate = d3.time.format("%Y-%m-%d").parse;
-        return x(parseDate(d));
-      }).y0(this.height).y1((function(_this) {
-        return function(d) {
-          if (_this.data[d] != null) {
-            return y(_this.data[d]);
-          } else {
-            return y(0);
-          }
-        };
-      })(this)).interpolate("basis");
-    };
-
-    ContributorsAuthorGraph.prototype.create_svg = function() {
-      this.list_item = d3.selectAll(".person")[0].pop();
-      return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
-    };
-
-    ContributorsAuthorGraph.prototype.draw_path = function(data) {
-      return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
-    };
-
-    ContributorsAuthorGraph.prototype.draw = function() {
-      this.create_scale();
-      this.create_axes();
-      this.set_domain();
-      this.create_area(this.x, this.y);
-      this.create_svg();
-      this.draw_path(this.dates);
-      this.draw_x_axis();
-      return this.draw_y_axis();
-    };
-
-    ContributorsAuthorGraph.prototype.redraw = function() {
-      this.set_domain();
-      this.svg.select("path").datum(this.dates);
-      this.svg.select("path").attr("d", this.area);
-      this.svg.select(".x.axis").call(this.x_axis);
-      return this.svg.select(".y.axis").call(this.y_axis);
-    };
-
-    return ContributorsAuthorGraph;
-  })(ContributorsGraph);
-}).call(window);
+      return x(parseDate(d));
+    }).y0(this.height).y1((function(_this) {
+      return function(d) {
+        if (_this.data[d] != null) {
+          return y(_this.data[d]);
+        } else {
+          return y(0);
+        }
+      };
+    })(this)).interpolate("basis");
+  };
+
+  ContributorsAuthorGraph.prototype.create_svg = function() {
+    this.list_item = d3.selectAll(".person")[0].pop();
+    return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+  };
+
+  ContributorsAuthorGraph.prototype.draw_path = function(data) {
+    return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+  };
+
+  ContributorsAuthorGraph.prototype.draw = function() {
+    this.create_scale();
+    this.create_axes();
+    this.set_domain();
+    this.create_area(this.x, this.y);
+    this.create_svg();
+    this.draw_path(this.dates);
+    this.draw_x_axis();
+    return this.draw_y_axis();
+  };
+
+  ContributorsAuthorGraph.prototype.redraw = function() {
+    this.set_domain();
+    this.svg.select("path").datum(this.dates);
+    this.svg.select("path").attr("d", this.area);
+    this.svg.select(".x.axis").call(this.x_axis);
+    return this.svg.select(".y.axis").call(this.y_axis);
+  };
+
+  return ContributorsAuthorGraph;
+})(ContributorsGraph);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 7954c583598fc55ed16562732e2f812fadd3c64c..c583757f3f2f44daa588a7ae33a481b867c1e761 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,138 +1,137 @@
 /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
-(function() {
-  window.ContributorsStatGraphUtil = {
-    parse_log: function(log) {
-      var by_author, by_email, data, entry, i, len, total, normalized_email;
-      total = {};
-      by_author = {};
-      by_email = {};
-      for (i = 0, len = log.length; i < len; i += 1) {
-        entry = log[i];
-        if (total[entry.date] == null) {
-          this.add_date(entry.date, total);
-        }
-        normalized_email = entry.author_email.toLowerCase();
-        data = by_author[entry.author_name] || by_email[normalized_email];
-        if (data == null) {
-          data = this.add_author(entry, by_author, by_email);
-        }
-        if (!data[entry.date]) {
-          this.add_date(entry.date, data);
-        }
-        this.store_data(entry, total[entry.date], data[entry.date]);
-      }
-      total = _.toArray(total);
-      by_author = _.toArray(by_author);
-      return {
-        total: total,
-        by_author: by_author
-      };
-    },
-    add_date: function(date, collection) {
-      collection[date] = {};
-      return collection[date].date = date;
-    },
-    add_author: function(author, by_author, by_email) {
-      var data, normalized_email;
-      data = {};
-      data.author_name = author.author_name;
-      data.author_email = author.author_email;
-      normalized_email = author.author_email.toLowerCase();
-      by_author[author.author_name] = data;
-      by_email[normalized_email] = data;
-      return data;
-    },
-    store_data: function(entry, total, by_author) {
-      this.store_commits(total, by_author);
-      this.store_additions(entry, total, by_author);
-      return this.store_deletions(entry, total, by_author);
-    },
-    store_commits: function(total, by_author) {
-      this.add(total, "commits", 1);
-      return this.add(by_author, "commits", 1);
-    },
-    add: function(collection, field, value) {
-      if (collection[field] == null) {
-        collection[field] = 0;
-      }
-      return collection[field] += value;
-    },
-    store_additions: function(entry, total, by_author) {
-      if (entry.additions == null) {
-        entry.additions = 0;
+
+export default {
+  parse_log: function(log) {
+    var by_author, by_email, data, entry, i, len, total, normalized_email;
+    total = {};
+    by_author = {};
+    by_email = {};
+    for (i = 0, len = log.length; i < len; i += 1) {
+      entry = log[i];
+      if (total[entry.date] == null) {
+        this.add_date(entry.date, total);
       }
-      this.add(total, "additions", entry.additions);
-      return this.add(by_author, "additions", entry.additions);
-    },
-    store_deletions: function(entry, total, by_author) {
-      if (entry.deletions == null) {
-        entry.deletions = 0;
+      normalized_email = entry.author_email.toLowerCase();
+      data = by_author[entry.author_name] || by_email[normalized_email];
+      if (data == null) {
+        data = this.add_author(entry, by_author, by_email);
       }
-      this.add(total, "deletions", entry.deletions);
-      return this.add(by_author, "deletions", entry.deletions);
-    },
-    get_total_data: function(parsed_log, field) {
-      var log, total_data;
-      log = parsed_log.total;
-      total_data = this.pick_field(log, field);
-      return _.sortBy(total_data, function(d) {
-        return d.date;
-      });
-    },
-    pick_field: function(log, field) {
-      var total_data;
-      total_data = [];
-      _.each(log, function(d) {
-        return total_data.push(_.pick(d, [field, 'date']));
-      });
-      return total_data;
-    },
-    get_author_data: function(parsed_log, field, date_range) {
-      var author_data, log;
-      if (date_range == null) {
-        date_range = null;
-      }
-      log = parsed_log.by_author;
-      author_data = [];
-      _.each(log, (function(_this) {
-        return function(log_entry) {
-          var parsed_log_entry;
-          parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
-          if (!_.isEmpty(parsed_log_entry.dates)) {
-            return author_data.push(parsed_log_entry);
-          }
-        };
-      })(this));
-      return _.sortBy(author_data, function(d) {
-        return d[field];
-      }).reverse();
-    },
-    parse_log_entry: function(log_entry, field, date_range) {
-      var parsed_entry;
-      parsed_entry = {};
-      parsed_entry.author_name = log_entry.author_name;
-      parsed_entry.author_email = log_entry.author_email;
-      parsed_entry.dates = {};
-      parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
-      _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
-        return function(value, key) {
-          if (_this.in_range(value.date, date_range)) {
-            parsed_entry.dates[value.date] = value[field];
-            parsed_entry.commits += value.commits;
-            parsed_entry.additions += value.additions;
-            return parsed_entry.deletions += value.deletions;
-          }
-        };
-      })(this));
-      return parsed_entry;
-    },
-    in_range: function(date, date_range) {
-      var ref;
-      if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
-        return true;
-      } else {
-        return false;
+      if (!data[entry.date]) {
+        this.add_date(entry.date, data);
       }
+      this.store_data(entry, total[entry.date], data[entry.date]);
+    }
+    total = _.toArray(total);
+    by_author = _.toArray(by_author);
+    return {
+      total: total,
+      by_author: by_author
+    };
+  },
+  add_date: function(date, collection) {
+    collection[date] = {};
+    return collection[date].date = date;
+  },
+  add_author: function(author, by_author, by_email) {
+    var data, normalized_email;
+    data = {};
+    data.author_name = author.author_name;
+    data.author_email = author.author_email;
+    normalized_email = author.author_email.toLowerCase();
+    by_author[author.author_name] = data;
+    by_email[normalized_email] = data;
+    return data;
+  },
+  store_data: function(entry, total, by_author) {
+    this.store_commits(total, by_author);
+    this.store_additions(entry, total, by_author);
+    return this.store_deletions(entry, total, by_author);
+  },
+  store_commits: function(total, by_author) {
+    this.add(total, "commits", 1);
+    return this.add(by_author, "commits", 1);
+  },
+  add: function(collection, field, value) {
+    if (collection[field] == null) {
+      collection[field] = 0;
+    }
+    return collection[field] += value;
+  },
+  store_additions: function(entry, total, by_author) {
+    if (entry.additions == null) {
+      entry.additions = 0;
+    }
+    this.add(total, "additions", entry.additions);
+    return this.add(by_author, "additions", entry.additions);
+  },
+  store_deletions: function(entry, total, by_author) {
+    if (entry.deletions == null) {
+      entry.deletions = 0;
+    }
+    this.add(total, "deletions", entry.deletions);
+    return this.add(by_author, "deletions", entry.deletions);
+  },
+  get_total_data: function(parsed_log, field) {
+    var log, total_data;
+    log = parsed_log.total;
+    total_data = this.pick_field(log, field);
+    return _.sortBy(total_data, function(d) {
+      return d.date;
+    });
+  },
+  pick_field: function(log, field) {
+    var total_data;
+    total_data = [];
+    _.each(log, function(d) {
+      return total_data.push(_.pick(d, [field, 'date']));
+    });
+    return total_data;
+  },
+  get_author_data: function(parsed_log, field, date_range) {
+    var author_data, log;
+    if (date_range == null) {
+      date_range = null;
+    }
+    log = parsed_log.by_author;
+    author_data = [];
+    _.each(log, (function(_this) {
+      return function(log_entry) {
+        var parsed_log_entry;
+        parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+        if (!_.isEmpty(parsed_log_entry.dates)) {
+          return author_data.push(parsed_log_entry);
+        }
+      };
+    })(this));
+    return _.sortBy(author_data, function(d) {
+      return d[field];
+    }).reverse();
+  },
+  parse_log_entry: function(log_entry, field, date_range) {
+    var parsed_entry;
+    parsed_entry = {};
+    parsed_entry.author_name = log_entry.author_name;
+    parsed_entry.author_email = log_entry.author_email;
+    parsed_entry.dates = {};
+    parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+    _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
+      return function(value, key) {
+        if (_this.in_range(value.date, date_range)) {
+          parsed_entry.dates[value.date] = value[field];
+          parsed_entry.commits += value.commits;
+          parsed_entry.additions += value.additions;
+          return parsed_entry.deletions += value.deletions;
+        }
+      };
+    })(this));
+    return parsed_entry;
+  },
+  in_range: function(date, date_range) {
+    var ref;
+    if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
+      return true;
+    } else {
+      return false;
     }
-  };
-}).call(window);
+  }
+};
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index c5cb273c5b28e75e99813dec45da5c836e5351dd..f03b47b1c1da46386f2971980beb7c33a5188909 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,20 +1,19 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
-  this.GroupAvatar = (function() {
-    function GroupAvatar() {
-      $('.js-choose-group-avatar-button').on("click", function() {
-        var form;
-        form = $(this).closest("form");
-        return form.find(".js-group-avatar-input").click();
-      });
-      $('.js-group-avatar-input').on("change", function() {
-        var filename, form;
-        form = $(this).closest("form");
-        filename = $(this).val().replace(/^.*[\\\/]/, '');
-        return form.find(".js-avatar-filename").text(filename);
-      });
-    }
 
-    return GroupAvatar;
-  })();
-}).call(window);
+window.GroupAvatar = (function() {
+  function GroupAvatar() {
+    $('.js-choose-group-avatar-button').on("click", function() {
+      var form;
+      form = $(this).closest("form");
+      return form.find(".js-group-avatar-input").click();
+    });
+    $('.js-group-avatar-input').on("change", function() {
+      var filename, form;
+      form = $(this).closest("form");
+      filename = $(this).val().replace(/^.*[\\\/]/, '');
+      return form.find(".js-avatar-filename").text(filename);
+    });
+  }
+
+  return GroupAvatar;
+})();
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
new file mode 100644
index 0000000000000000000000000000000000000000..7dc9ce898e86c663b9d688603106cdc43a145d57
--- /dev/null
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -0,0 +1,52 @@
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
+
+class GroupLabelSubscription {
+  constructor(container) {
+    const $container = $(container);
+    this.$dropdown = $container.find('.dropdown');
+    this.$subscribeButtons = $container.find('.js-subscribe-button');
+    this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
+
+    this.$subscribeButtons.on('click', this.subscribe.bind(this));
+    this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
+  }
+
+  unsubscribe(event) {
+    event.preventDefault();
+
+    const url = this.$unsubscribeButtons.attr('data-url');
+
+    $.ajax({
+      type: 'POST',
+      url: url
+    }).done(() => {
+      this.toggleSubscriptionButtons();
+      this.$unsubscribeButtons.removeAttr('data-url');
+    });
+  }
+
+  subscribe(event) {
+    event.preventDefault();
+
+    const $btn = $(event.currentTarget);
+    const url = $btn.attr('data-url');
+
+    this.$unsubscribeButtons.attr('data-url', url);
+
+    $.ajax({
+      type: 'POST',
+      url: url
+    }).done(() => {
+      this.toggleSubscriptionButtons();
+    });
+  }
+
+  toggleSubscriptionButtons() {
+    this.$dropdown.toggleClass('hidden');
+    this.$subscribeButtons.toggleClass('hidden');
+    this.$unsubscribeButtons.toggleClass('hidden');
+  }
+}
+
+window.gl = window.gl || {};
+window.gl.GroupLabelSubscription = GroupLabelSubscription;
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
deleted file mode 100644
index 15e695e81cfdba0563d897c2128c1ad2af371526..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
-
-(function(global) {
-  class GroupLabelSubscription {
-    constructor(container) {
-      const $container = $(container);
-      this.$dropdown = $container.find('.dropdown');
-      this.$subscribeButtons = $container.find('.js-subscribe-button');
-      this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
-
-      this.$subscribeButtons.on('click', this.subscribe.bind(this));
-      this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
-    }
-
-    unsubscribe(event) {
-      event.preventDefault();
-
-      const url = this.$unsubscribeButtons.attr('data-url');
-
-      $.ajax({
-        type: 'POST',
-        url: url
-      }).done(() => {
-        this.toggleSubscriptionButtons();
-        this.$unsubscribeButtons.removeAttr('data-url');
-      });
-    }
-
-    subscribe(event) {
-      event.preventDefault();
-
-      const $btn = $(event.currentTarget);
-      const url = $btn.attr('data-url');
-
-      this.$unsubscribeButtons.attr('data-url', url);
-
-      $.ajax({
-        type: 'POST',
-        url: url
-      }).done(() => {
-        this.toggleSubscriptionButtons();
-      });
-    }
-
-    toggleSubscriptionButtons() {
-      this.$dropdown.toggleClass('hidden');
-      this.$subscribeButtons.toggleClass('hidden');
-      this.$unsubscribeButtons.toggleClass('hidden');
-    }
-  }
-
-  global.GroupLabelSubscription = GroupLabelSubscription;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a028f299b16230d0374ed5c45542211e8e32a6a
--- /dev/null
+++ b/app/assets/javascripts/group_name.js
@@ -0,0 +1,40 @@
+const GROUP_LIMIT = 2;
+
+export default class GroupName {
+  constructor() {
+    this.titleContainer = document.querySelector('.title');
+    this.groups = document.querySelectorAll('.group-path');
+    this.groupTitle = document.querySelector('.group-title');
+    this.toggle = null;
+    this.isHidden = false;
+    this.init();
+  }
+
+  init() {
+    if (this.groups.length > GROUP_LIMIT) {
+      this.groups[this.groups.length - 1].classList.remove('hidable');
+      this.addToggle();
+    }
+    this.render();
+  }
+
+  addToggle() {
+    const header = document.querySelector('.header-content');
+    this.toggle = document.createElement('button');
+    this.toggle.className = 'text-expander group-name-toggle';
+    this.toggle.setAttribute('aria-label', 'Toggle full path');
+    this.toggle.innerHTML = '...';
+    this.toggle.addEventListener('click', this.toggleGroups.bind(this));
+    header.insertBefore(this.toggle, this.titleContainer);
+    this.toggleGroups();
+  }
+
+  toggleGroups() {
+    this.isHidden = !this.isHidden;
+    this.groupTitle.classList.toggle('is-hidden');
+  }
+
+  render() {
+    this.titleContainer.classList.remove('initializing');
+  }
+}
diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js
new file mode 100644
index 0000000000000000000000000000000000000000..56a8cbf6d037fc0d726e58736ea101d7a9d40f98
--- /dev/null
+++ b/app/assets/javascripts/groups_list.js
@@ -0,0 +1,18 @@
+import FilterableList from './filterable_list';
+
+/**
+ * Makes search request for groups when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+export default class GroupsList {
+  constructor() {
+    const form = document.querySelector('form#group-filter-form');
+    const filter = document.querySelector('.js-groups-list-filter');
+    const holder = document.querySelector('.js-groups-list-holder');
+
+    if (form && filter && holder) {
+      const list = new FilterableList(form, filter, holder);
+      list.initSearch();
+    }
+  }
+}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 6b937e7fa0f1047594bc6e4bcd85d2c108fa3d7f..e5dfa30edab6cfc6e379c9f689a8e62a194279af 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,71 +1,69 @@
 /* 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, max-len */
 /* global Api */
 
-(function() {
-  var slice = [].slice;
+var slice = [].slice;
 
-  this.GroupsSelect = (function() {
-    function GroupsSelect() {
-      $('.ajax-groups-select').each((function(_this) {
-        return function(i, select) {
-          var all_available, skip_groups;
-          all_available = $(select).data('all-available');
-          skip_groups = $(select).data('skip-groups') || [];
-          return $(select).select2({
-            placeholder: "Search for a group",
-            multiple: $(select).hasClass('multiselect'),
-            minimumInputLength: 0,
-            query: function(query) {
-              var options = { all_available: all_available, skip_groups: skip_groups };
-              return Api.groups(query.term, options, function(groups) {
-                var data;
-                data = {
-                  results: groups
-                };
-                return query.callback(data);
-              });
-            },
-            initSelection: function(element, callback) {
-              var id;
-              id = $(element).val();
-              if (id !== "") {
-                return Api.group(id, callback);
-              }
-            },
-            formatResult: function() {
-              var args;
-              args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
-              return _this.formatResult.apply(_this, args);
-            },
-            formatSelection: function() {
-              var args;
-              args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
-              return _this.formatSelection.apply(_this, args);
-            },
-            dropdownCssClass: "ajax-groups-dropdown",
-            // we do not want to escape markup since we are displaying html in results
-            escapeMarkup: function(m) {
-              return m;
+window.GroupsSelect = (function() {
+  function GroupsSelect() {
+    $('.ajax-groups-select').each((function(_this) {
+      return function(i, select) {
+        var all_available, skip_groups;
+        all_available = $(select).data('all-available');
+        skip_groups = $(select).data('skip-groups') || [];
+        return $(select).select2({
+          placeholder: "Search for a group",
+          multiple: $(select).hasClass('multiselect'),
+          minimumInputLength: 0,
+          query: function(query) {
+            var options = { all_available: all_available, skip_groups: skip_groups };
+            return Api.groups(query.term, options, function(groups) {
+              var data;
+              data = {
+                results: groups
+              };
+              return query.callback(data);
+            });
+          },
+          initSelection: function(element, callback) {
+            var id;
+            id = $(element).val();
+            if (id !== "") {
+              return Api.group(id, callback);
             }
-          });
-        };
-      })(this));
-    }
+          },
+          formatResult: function() {
+            var args;
+            args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+            return _this.formatResult.apply(_this, args);
+          },
+          formatSelection: function() {
+            var args;
+            args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+            return _this.formatSelection.apply(_this, args);
+          },
+          dropdownCssClass: "ajax-groups-dropdown",
+          // we do not want to escape markup since we are displaying html in results
+          escapeMarkup: function(m) {
+            return m;
+          }
+        });
+      };
+    })(this));
+  }
 
-    GroupsSelect.prototype.formatResult = function(group) {
-      var avatar;
-      if (group.avatar_url) {
-        avatar = group.avatar_url;
-      } else {
-        avatar = gon.default_avatar_url;
-      }
-      return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
-    };
+  GroupsSelect.prototype.formatResult = function(group) {
+    var avatar;
+    if (group.avatar_url) {
+      avatar = group.avatar_url;
+    } else {
+      avatar = gon.default_avatar_url;
+    }
+    return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
+  };
 
-    GroupsSelect.prototype.formatSelection = function(group) {
-      return group.full_name;
-    };
+  GroupsSelect.prototype.formatSelection = function(group) {
+    return group.full_name;
+  };
 
-    return GroupsSelect;
-  })();
-}).call(window);
+  return GroupsSelect;
+})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index a853c3aeb1fa63052fe924c4ca64e2f92d9fea68..34f44dad7a591ee2177b5e50dcd67bb9f462d53d 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,8 +1,7 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
-(function() {
-  $(document).on('todo:toggle', function(e, count) {
-    var $todoPendingCount = $('.todos-pending-count');
-    $todoPendingCount.text(gl.text.highCountTrim(count));
-    $todoPendingCount.toggleClass('hidden', count === 0);
-  });
-})();
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
+
+$(document).on('todo:toggle', function(e, count) {
+  var $todoPendingCount = $('.todos-pending-count');
+  $todoPendingCount.text(gl.text.highCountTrim(count));
+  $todoPendingCount.toggleClass('hidden', count === 0);
+});
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js
similarity index 98%
rename from app/assets/javascripts/issuable.js.es6
rename to app/assets/javascripts/issuable.js
index 8df86f682183bf54c47a72b957fe330ff01d7479..3bfce32768a6923255a1809d3163447fbdce91fe 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js
@@ -116,7 +116,7 @@
         formData = $.param(formData);
         formAction = form.attr('action');
         issuesUrl = formAction;
-        issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
+        issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
         issuesUrl += formData;
         return gl.utils.visitUrl(issuesUrl);
       };
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js
similarity index 100%
rename from app/assets/javascripts/issuable/issuable_bundle.js.es6
rename to app/assets/javascripts/issuable/issuable_bundle.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
similarity index 94%
rename from app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
index bf27fbac5d79f806802a7a811d45784ca05831df..357b3487ca90f31b4a043d880754ed18c63d2d02 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
@@ -1,4 +1,6 @@
 /* global Vue */
+import stopwatchSvg from 'icons/_icon_stopwatch.svg';
+
 require('../../../lib/utils/pretty_time');
 
 (() => {
@@ -11,7 +13,6 @@ require('../../../lib/utils/pretty_time');
       'showNoTimeTrackingState',
       'timeSpentHumanReadable',
       'timeEstimateHumanReadable',
-      'stopwatchSvg',
     ],
     methods: {
       abbreviateTime(timeStr) {
@@ -20,7 +21,7 @@ require('../../../lib/utils/pretty_time');
     },
     template: `
       <div class='sidebar-collapsed-icon'>
-        <div v-html='stopwatchSvg'></div>
+        ${stopwatchSvg}
         <div class='time-tracking-collapsed-summary'>
           <div class='compare' v-if='showComparisonState'>
             <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
similarity index 100%
rename from app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
similarity index 100%
rename from app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
similarity index 100%
rename from app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/help_state.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
similarity index 100%
rename from app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
similarity index 100%
rename from app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
similarity index 95%
rename from app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
rename to app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
index e38f7852b1cac99a799e108b90660b1169e884c3..1fae2d62b143b01b31b218fe027edd3d5c607fb8 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
@@ -15,7 +15,6 @@ require('./comparison_pane');
       'time_spent',
       'human_time_estimate',
       'human_time_spent',
-      'stopwatchSvg',
       'docsUrl',
     ],
     data() {
@@ -71,20 +70,19 @@ require('./comparison_pane');
           :show-spent-only-state='showSpentOnlyState'
           :show-estimate-only-state='showEstimateOnlyState'
           :time-spent-human-readable='timeSpentHumanReadable'
-          :time-estimate-human-readable='timeEstimateHumanReadable'
-          :stopwatch-svg='stopwatchSvg'>
+          :time-estimate-human-readable='timeEstimateHumanReadable'>
         </time-tracking-collapsed-state>
         <div class='title hide-collapsed'>
           Time tracking
           <div class='help-button pull-right'
             v-if='!showHelpState'
             @click='toggleHelpState(true)'>
-            <i class='fa fa-question-circle'></i>
+            <i class='fa fa-question-circle' aria-hidden='true'></i>
           </div>
           <div class='close-help-button pull-right'
             v-if='showHelpState'
             @click='toggleHelpState(false)'>
-            <i class='fa fa-close'></i>
+            <i class='fa fa-close' aria-hidden='true'></i>
           </div>
         </div>
         <div class='time-tracking-content hide-collapsed'>
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
similarity index 90%
rename from app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
rename to app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
index 1ca01d3bdb9c44e2d3c78272f65a02efb34a1c88..0134b7cb6f3b9c233e8ac1dd92edb69fd6414a5d 100644
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
@@ -1,5 +1,7 @@
 /* global Vue */
 
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
 require('./components/time_tracker');
 require('../../smart_interval');
 require('../../subbable_resource');
@@ -39,8 +41,9 @@ require('../../subbable_resource');
           listenForSlashCommands() {
             $(document).on('ajax:success', '.gfm-form', (e, data) => {
               const subscribedCommands = ['spend_time', 'time_estimate'];
-              const changedCommands = data.commands_changes;
-
+              const changedCommands = data.commands_changes
+                ? Object.keys(data.commands_changes)
+                : [];
               if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
                 this.fetchIssuable();
               }
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 52457f70d908c447bb789009b936f6283ce9fc1e..47e675f537e94af46e9fea18f774dd8efc271d45 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,134 +2,130 @@
 /* global Flash */
 
 require('./flash');
+require('~/lib/utils/text_utility');
 require('vendor/jquery.waitforimages');
 require('./task_list');
 
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
-  this.Issue = (function() {
-    function Issue() {
-      this.submitNoteForm = bind(this.submitNoteForm, this);
-      if ($('a.btn-close').length) {
-        this.taskList = new gl.TaskList({
-          dataType: 'issue',
-          fieldName: 'description',
-          selector: '.detail-page-description',
-          onSuccess: (result) => {
-            document.querySelector('#task_status').innerText = result.task_status;
-            document.querySelector('#task_status_short').innerText = result.task_status_short;
-          }
-        });
-        this.initIssueBtnEventListeners();
-      }
-      this.initMergeRequests();
-      this.initRelatedBranches();
-      this.initCanCreateBranch();
+class Issue {
+  constructor() {
+    if ($('a.btn-close').length) {
+      this.taskList = new gl.TaskList({
+        dataType: 'issue',
+        fieldName: 'description',
+        selector: '.detail-page-description',
+        onSuccess: (result) => {
+          document.querySelector('#task_status').innerText = result.task_status;
+          document.querySelector('#task_status_short').innerText = result.task_status_short;
+        }
+      });
+      Issue.initIssueBtnEventListeners();
     }
+    Issue.initMergeRequests();
+    Issue.initRelatedBranches();
+    Issue.initCanCreateBranch();
+  }
 
-    Issue.prototype.initIssueBtnEventListeners = function() {
-      var _this, issueFailMessage;
-      _this = this;
-      issueFailMessage = 'Unable to update this issue at this time.';
-      return $('a.btn-close, a.btn-reopen').on('click', function(e) {
-        var $this, isClose, shouldSubmit, url;
-        e.preventDefault();
-        e.stopImmediatePropagation();
-        $this = $(this);
-        isClose = $this.hasClass('btn-close');
-        shouldSubmit = $this.hasClass('btn-comment');
-        if (shouldSubmit) {
-          _this.submitNoteForm($this.closest('form'));
-        }
-        $this.prop('disabled', true);
-        url = $this.attr('href');
-        return $.ajax({
-          type: 'PUT',
-          url: url,
-          error: function(jqXHR, textStatus, errorThrown) {
-            var issueStatus;
-            issueStatus = isClose ? 'close' : 'open';
-            return new Flash(issueFailMessage, 'alert');
-          },
-          success: function(data, textStatus, jqXHR) {
-            if ('id' in data) {
-              $(document).trigger('issuable:change');
-              const currentTotal = Number($('.issue_counter').text());
-              if (isClose) {
-                $('a.btn-close').addClass('hidden');
-                $('a.btn-reopen').removeClass('hidden');
-                $('div.status-box-closed').removeClass('hidden');
-                $('div.status-box-open').addClass('hidden');
-                $('.issue_counter').text(currentTotal - 1);
-              } else {
-                $('a.btn-reopen').addClass('hidden');
-                $('a.btn-close').removeClass('hidden');
-                $('div.status-box-closed').addClass('hidden');
-                $('div.status-box-open').removeClass('hidden');
-                $('.issue_counter').text(currentTotal + 1);
-              }
+  static initIssueBtnEventListeners() {
+    var issueFailMessage;
+    issueFailMessage = 'Unable to update this issue at this time.';
+    return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+      var $this, isClose, shouldSubmit, url;
+      e.preventDefault();
+      e.stopImmediatePropagation();
+      $this = $(this);
+      isClose = $this.hasClass('btn-close');
+      shouldSubmit = $this.hasClass('btn-comment');
+      if (shouldSubmit) {
+        Issue.submitNoteForm($this.closest('form'));
+      }
+      $this.prop('disabled', true);
+      url = $this.attr('href');
+      return $.ajax({
+        type: 'PUT',
+        url: url,
+        error: function(jqXHR, textStatus, errorThrown) {
+          var issueStatus;
+          issueStatus = isClose ? 'close' : 'open';
+          return new Flash(issueFailMessage, 'alert');
+        },
+        success: function(data, textStatus, jqXHR) {
+          if ('id' in data) {
+            $(document).trigger('issuable:change');
+            let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
+            if (isClose) {
+              $('a.btn-close').addClass('hidden');
+              $('a.btn-reopen').removeClass('hidden');
+              $('div.status-box-closed').removeClass('hidden');
+              $('div.status-box-open').addClass('hidden');
+              total -= 1;
             } else {
-              new Flash(issueFailMessage, 'alert');
+              $('a.btn-reopen').addClass('hidden');
+              $('a.btn-close').removeClass('hidden');
+              $('div.status-box-closed').addClass('hidden');
+              $('div.status-box-open').removeClass('hidden');
+              total += 1;
             }
-            return $this.prop('disabled', false);
+            $('.issue_counter').text(gl.text.addDelimiter(total));
+          } else {
+            new Flash(issueFailMessage, 'alert');
           }
-        });
+          return $this.prop('disabled', false);
+        }
       });
-    };
+    });
+  }
 
-    Issue.prototype.submitNoteForm = function(form) {
-      var noteText;
-      noteText = form.find("textarea.js-note-text").val();
-      if (noteText.trim().length > 0) {
-        return form.submit();
-      }
-    };
+  static submitNoteForm(form) {
+    var noteText;
+    noteText = form.find("textarea.js-note-text").val();
+    if (noteText.trim().length > 0) {
+      return form.submit();
+    }
+  }
 
-    Issue.prototype.initMergeRequests = function() {
-      var $container;
-      $container = $('#merge-requests');
-      return $.getJSON($container.data('url')).error(function() {
-        return new Flash('Failed to load referenced merge requests', 'alert');
-      }).success(function(data) {
-        if ('html' in data) {
-          return $container.html(data.html);
-        }
-      });
-    };
+  static initMergeRequests() {
+    var $container;
+    $container = $('#merge-requests');
+    return $.getJSON($container.data('url')).error(function() {
+      return new Flash('Failed to load referenced merge requests', 'alert');
+    }).success(function(data) {
+      if ('html' in data) {
+        return $container.html(data.html);
+      }
+    });
+  }
 
-    Issue.prototype.initRelatedBranches = function() {
-      var $container;
-      $container = $('#related-branches');
-      return $.getJSON($container.data('url')).error(function() {
-        return new Flash('Failed to load related branches', 'alert');
-      }).success(function(data) {
-        if ('html' in data) {
-          return $container.html(data.html);
-        }
-      });
-    };
+  static initRelatedBranches() {
+    var $container;
+    $container = $('#related-branches');
+    return $.getJSON($container.data('url')).error(function() {
+      return new Flash('Failed to load related branches', 'alert');
+    }).success(function(data) {
+      if ('html' in data) {
+        return $container.html(data.html);
+      }
+    });
+  }
 
-    Issue.prototype.initCanCreateBranch = function() {
-      var $container;
-      $container = $('#new-branch');
-      // If the user doesn't have the required permissions the container isn't
-      // rendered at all.
-      if ($container.length === 0) {
-        return;
+  static initCanCreateBranch() {
+    var $container;
+    $container = $('#new-branch');
+    // If the user doesn't have the required permissions the container isn't
+    // rendered at all.
+    if ($container.length === 0) {
+      return;
+    }
+    return $.getJSON($container.data('path')).error(function() {
+      $container.find('.unavailable').show();
+      return new Flash('Failed to check if a new branch can be created.', 'alert');
+    }).success(function(data) {
+      if (data.can_create_branch) {
+        $container.find('.available').show();
+      } else {
+        return $container.find('.unavailable').show();
       }
-      return $.getJSON($container.data('path')).error(function() {
-        $container.find('.unavailable').show();
-        return new Flash('Failed to check if a new branch can be created.', 'alert');
-      }).success(function(data) {
-        if (data.can_create_branch) {
-          $container.find('.available').show();
-        } else {
-          return $container.find('.unavailable').show();
-        }
-      });
-    };
+    });
+  }
+}
 
-    return Issue;
-  })();
-}).call(window);
+export default Issue;
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js
similarity index 100%
rename from app/assets/javascripts/issues_bulk_assignment.js.es6
rename to app/assets/javascripts/issues_bulk_assignment.js
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js
similarity index 100%
rename from app/assets/javascripts/label_manager.js.es6
rename to app/assets/javascripts/label_manager.js
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9e2d14c7f8776995e19b718ed8333ca7ee90f181..443fb3e0ca96c183148c2f8f3076b31888495aec 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
           if (!selected.length) {
             data[abilityName].label_ids = [''];
           }
-          $loading.fadeIn();
+          $loading.removeClass('hidden').fadeIn();
           $dropdown.trigger('loading.gl.dropdown');
           return $.ajax({
             type: 'PUT',
@@ -353,31 +353,17 @@
               return;
             }
 
-            if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
-              !$dropdown.closest('.add-issues-modal').length) {
-              boardsModel = gl.issueBoards.BoardsStore.state.filters;
-            } else if ($dropdown.closest('.add-issues-modal').length) {
+            if ($dropdown.closest('.add-issues-modal').length) {
               boardsModel = gl.issueBoards.ModalStore.store.filter;
             }
 
             if (boardsModel) {
               if (label.isAny) {
                 boardsModel['label_name'] = [];
-              }
-              else if ($el.hasClass('is-active')) {
+              } else if ($el.hasClass('is-active')) {
                 boardsModel['label_name'].push(label.title);
               }
-              else {
-                var filters = boardsModel['label_name'];
-                filters = filters.filter(function (filteredLabel) {
-                  return filteredLabel !== label.title;
-                });
-                boardsModel['label_name'] = filters;
-              }
 
-              if (!$dropdown.closest('.add-issues-modal').length) {
-                gl.issueBoards.BoardsStore.updateFiltersUrl();
-              }
               e.preventDefault();
               return;
             }
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
deleted file mode 100644
index 9b011d89e939723ef5db7765b6163a6daaf4c813..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/chart.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-window.Chart = require('vendor/Chart');
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
deleted file mode 100644
index 7862c6797c3366a33ddebda89a491de4f8d34740..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/cropper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require cropper */
-
-(function() {
-
-}).call(window);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
deleted file mode 100644
index a9dd32edbed98f8cdd16bb71a455c684bfb0cbc5..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/d3.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-window.d3 = require('d3');
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
deleted file mode 100644
index ebe1e2ae98dc92bfd442683f6597f4c26b93788d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/raphael.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require raphael */
-/*= require g.raphael */
-/*= require g.bar */
-
-(function() {
-
-}).call(window);
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
similarity index 100%
rename from app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
rename to app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js
similarity index 85%
rename from app/assets/javascripts/lib/utils/common_utils.js.es6
rename to app/assets/javascripts/lib/utils/common_utils.js
index 45a1d90a9d997107780f3d77b97744fee3f976d3..a1423b6fda560b5566d695eda0de1c3f0b52467a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -246,17 +246,6 @@
       previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
     });
 
-    /**
-     * Transforms a DOMStringMap into a plain object.
-     *
-     * @param {DOMStringMap} DOMStringMapObject
-     * @returns {Object}
-     */
-    w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
-      acc[element] = DOMStringMapObject[element];
-      return acc;
-    }, {});
-
     /**
      * Updates the search parameter of a URL given the parameter and values provided.
      *
@@ -296,5 +285,58 @@
      * @returns {Boolean}
      */
     w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
+
+    /**
+     * Back Off exponential algorithm
+     * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
+     *
+     * @param {Function<next, stop>} fn function to be called
+     * @param {Number} timeout
+     * @return {Promise<Any, Error>}
+     * @example
+     * ```
+     *  backOff(function (next, stop) {
+     *    // Let's perform this function repeatedly for 60s or for the timeout provided.
+     *
+     *    ourFunction()
+     *      .then(function (result) {
+     *        // continue if result is not what we need
+     *        next();
+     *
+     *        // when result is what we need let's stop with the repetions and jump out of the cycle
+     *        stop(result);
+     *      })
+     *      .catch(function (error) {
+     *        // if there is an error, we need to stop this with an error.
+     *        stop(error);
+     *      })
+     *  }, 60000)
+     *  .then(function (result) {})
+     *  .catch(function (error) {
+     *    // deal with errors passed to stop()
+     *  })
+     * ```
+     */
+    w.gl.utils.backOff = (fn, timeout = 60000) => {
+      const maxInterval = 32000;
+      let nextInterval = 2000;
+
+      const startTime = Date.now();
+
+      return new Promise((resolve, reject) => {
+        const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+
+        const next = () => {
+          if (Date.now() - startTime < timeout) {
+            setTimeout(fn.bind(null, next, stop), nextInterval);
+            nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
+          } else {
+            reject(new Error('BACKOFF_TIMEOUT'));
+          }
+        };
+
+        fn(next, stop);
+      });
+    };
   })(window);
 }).call(window);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js
similarity index 100%
rename from app/assets/javascripts/lib/utils/datetime_utility.js.es6
rename to app/assets/javascripts/lib/utils/datetime_utility.js
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc109a69c20b95375d86e21a40b41cabb0d28d25
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -0,0 +1,10 @@
+/**
+ * exports HTTP status codes
+ */
+
+const statusCodes = {
+  NO_CONTENT: 204,
+  OK: 200,
+};
+
+module.exports = statusCodes;
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js
similarity index 100%
rename from app/assets/javascripts/lib/utils/pretty_time.js.es6
rename to app/assets/javascripts/lib/utils/pretty_time.js
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 579d322e3fb13303af2c019fee02d9a87cb79b67..2e5f8a09fc1ba8642957d75feead5ee499379c09 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -65,9 +65,10 @@ require('vendor/latinise');
       }
     };
     gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
-      var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine;
+      var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
       removedLastNewLine = false;
       removedFirstNewLine = false;
+      currentLineEmpty = false;
 
       // Remove the first newline
       if (selected.indexOf('\n') === 0) {
@@ -82,7 +83,17 @@ require('vendor/latinise');
       }
 
       selectedSplit = selected.split('\n');
-      startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
+
+      if (!wrap) {
+        lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+        // Check whether the current line is empty or consists only of spaces(=handle as empty)
+        if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+          currentLineEmpty = true;
+        }
+      }
+
+      startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
 
       if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
         if (blockTag != null) {
@@ -142,9 +153,8 @@ require('vendor/latinise');
       }
     };
     gl.text.updateText = function(textArea, tag, blockTag, wrap) {
-      var $textArea, oldVal, selected, text;
+      var $textArea, selected, text;
       $textArea = $(textArea);
-      oldVal = $textArea.val();
       textArea = $textArea.get(0);
       text = $textArea.val();
       selected = this.selectedText(text, textArea);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js
similarity index 92%
rename from app/assets/javascripts/lib/utils/url_utility.js.es6
rename to app/assets/javascripts/lib/utils/url_utility.js
index 1bc81d2e4a4e320074cd0ed1e49ed7d64794262c..09c4261b3187d25d5486220db2a7035844749b7f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js.es6
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -66,6 +66,13 @@
         return results;
       })()).join('&');
     };
+    w.gl.utils.removeParams = (params) => {
+      const url = new URL(window.location.href);
+      params.forEach((param) => {
+        url.search = w.gl.utils.removeParamQueryString(url.search, param);
+      });
+      return url.href;
+    };
     w.gl.utils.getLocationHash = function(url) {
       var hashIndex;
       if (typeof url === 'undefined') {
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
deleted file mode 100644
index 49babdea2e180ae95d6e5484ea62015a95be4697..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/vue_resource.js.es6
+++ /dev/null
@@ -1,2 +0,0 @@
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 966fcd8ec4774ae39e5c992e4aee028fe24f4f1c..1821ca1805376dcc647238d951c4b1713724768f 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo');
     }
 
     LineHighlighter.prototype.bindEvents = function() {
-      $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
-      // While it may seem odd to bind to the mousedown event and then throw away
-      // the click event, there is a method to our madness.
-      //
-      // If not done this way, the line number anchor will sometimes keep its
-      // active state even when the event is cancelled, resulting in an ugly border
-      // around the link and/or a persisted underline text decoration.
-      $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
-        event.preventDefault();
-        event.stopPropagation();
-      });
+      $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
     };
 
     LineHighlighter.prototype.clickHandler = function(event) {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..81d5748191d0c75648b4cb752750117d97b86ca6
--- /dev/null
+++ b/app/assets/javascripts/main.js
@@ -0,0 +1,384 @@
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* global Aside */
+
+import jQuery from 'jquery';
+import _ from 'underscore';
+import Cookies from 'js-cookie';
+import Pikaday from 'pikaday';
+import Dropzone from 'dropzone';
+import Sortable from 'vendor/Sortable';
+
+// libraries with import side-effects
+import 'mousetrap';
+import 'mousetrap/plugins/pause/mousetrap-pause';
+import 'vendor/fuzzaldrin-plus';
+
+// extensions
+import './extensions/array';
+
+// expose common libraries as globals (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+window._ = _;
+window.Cookies = Cookies;
+window.Pikaday = Pikaday;
+window.Dropzone = Dropzone;
+window.Sortable = Sortable;
+
+// shortcuts
+import './shortcuts';
+import './shortcuts_blob';
+import './shortcuts_dashboard_navigation';
+import './shortcuts_navigation';
+import './shortcuts_find_file';
+import './shortcuts_issuable';
+import './shortcuts_network';
+
+// behaviors
+import './behaviors/autosize';
+import './behaviors/details_behavior';
+import './behaviors/quick_submit';
+import './behaviors/requires_input';
+import './behaviors/toggler_behavior';
+import './behaviors/bind_in_out';
+import { installGlEmojiElement } from './behaviors/gl_emoji';
+installGlEmojiElement();
+
+// blob
+import './blob/blob_ci_yaml';
+import './blob/blob_dockerfile_selector';
+import './blob/blob_dockerfile_selectors';
+import './blob/blob_file_dropzone';
+import './blob/blob_gitignore_selector';
+import './blob/blob_gitignore_selectors';
+import './blob/blob_license_selector';
+import './blob/blob_license_selectors';
+import './blob/template_selector';
+import './blob/create_branch_dropdown';
+import './blob/target_branch_dropdown';
+
+// templates
+import './templates/issuable_template_selector';
+import './templates/issuable_template_selectors';
+
+// commit
+import './commit/file';
+import './commit/image_file';
+
+// lib/utils
+import './lib/utils/animate';
+import './lib/utils/bootstrap_linked_tabs';
+import './lib/utils/common_utils';
+import './lib/utils/datetime_utility';
+import './lib/utils/notify';
+import './lib/utils/pretty_time';
+import './lib/utils/text_utility';
+import './lib/utils/type_utility';
+import './lib/utils/url_utility';
+
+// u2f
+import './u2f/authenticate';
+import './u2f/error';
+import './u2f/register';
+import './u2f/util';
+
+// droplab
+import './droplab/droplab';
+import './droplab/droplab_ajax';
+import './droplab/droplab_ajax_filter';
+import './droplab/droplab_filter';
+
+// everything else
+import './abuse_reports';
+import './activities';
+import './admin';
+import './ajax_loading_spinner';
+import './api';
+import './aside';
+import './autosave';
+import AwardsHandler from './awards_handler';
+import './breakpoints';
+import './broadcast_message';
+import './build';
+import './build_artifacts';
+import './build_variables';
+import './ci_lint_editor';
+import './commit';
+import './commits';
+import './compare';
+import './compare_autocomplete';
+import './confirm_danger_modal';
+import './copy_as_gfm';
+import './copy_to_clipboard';
+import './create_label';
+import './diff';
+import './dispatcher';
+import './dropzone_input';
+import './due_date_select';
+import './files_comment_button';
+import './flash';
+import './gfm_auto_complete';
+import './gl_dropdown';
+import './gl_field_error';
+import './gl_field_errors';
+import './gl_form';
+import './group_avatar';
+import './group_label_subscription';
+import './groups_select';
+import './header';
+import './importer_status';
+import './issuable';
+import './issuable_context';
+import './issuable_form';
+import './issue';
+import './issue_status_select';
+import './issues_bulk_assignment';
+import './label_manager';
+import './labels';
+import './labels_select';
+import './layout_nav';
+import './line_highlighter';
+import './logo';
+import './member_expiration_date';
+import './members';
+import './merge_request';
+import './merge_request_tabs';
+import './merge_request_widget';
+import './merged_buttons';
+import './milestone';
+import './milestone_select';
+import './mini_pipeline_graph_dropdown';
+import './namespace_select';
+import './new_branch_form';
+import './new_commit_form';
+import './notes';
+import './notifications_dropdown';
+import './notifications_form';
+import './pager';
+import './pipelines';
+import './preview_markdown';
+import './project';
+import './project_avatar';
+import './project_find_file';
+import './project_fork';
+import './project_import';
+import './project_label_subscription';
+import './project_new';
+import './project_select';
+import './project_show';
+import './project_variables';
+import './projects_list';
+import './render_gfm';
+import './render_math';
+import './right_sidebar';
+import './search';
+import './search_autocomplete';
+import './signin_tabs_memoizer';
+import './single_file_diff';
+import './smart_interval';
+import './snippets_list';
+import './star';
+import './subbable_resource';
+import './subscription';
+import './subscription_select';
+import './syntax_highlight';
+import './task_list';
+import './todos';
+import './tree';
+import './user';
+import './user_tabs';
+import './username_validator';
+import './users_select';
+import './version_check_image';
+import './visibility_select';
+import './wikis';
+import './zen_mode';
+
+document.addEventListener('beforeunload', function () {
+  // Unbind scroll events
+  $(document).off('scroll');
+  // Close any open tooltips
+  $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+});
+
+window.addEventListener('hashchange', gl.utils.handleLocationHash);
+window.addEventListener('load', function onLoad() {
+  window.removeEventListener('load', onLoad, false);
+  gl.utils.handleLocationHash();
+}, false);
+
+$(function () {
+  var $body = $('body');
+  var $document = $(document);
+  var $window = $(window);
+  var $sidebarGutterToggle = $('.js-sidebar-toggle');
+  var $flash = $('.flash-container');
+  var bootstrapBreakpoint = bp.getBreakpointSize();
+  var fitSidebarForSize;
+
+  // Set the default path for all cookies to GitLab's root directory
+  Cookies.defaults.path = gon.relative_url_root || '/';
+
+  // `hashchange` is not triggered when link target is already in window.location
+  $body.on('click', 'a[href^="#"]', function() {
+    var href = this.getAttribute('href');
+    if (href.substr(1) === gl.utils.getLocationHash()) {
+      setTimeout(gl.utils.handleLocationHash, 1);
+    }
+  });
+
+  // prevent default action for disabled buttons
+  $('.btn').click(function(e) {
+    if ($(this).hasClass('disabled')) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+      return false;
+    }
+  });
+
+  $('.js-select-on-focus').on('focusin', function () {
+    return $(this).select().one('mouseup', function (e) {
+      return e.preventDefault();
+    });
+  // Click a .js-select-on-focus field, select the contents
+  // Prevent a mouseup event from deselecting the input
+  });
+  $('.remove-row').bind('ajax:success', function () {
+    $(this).tooltip('destroy')
+      .closest('li')
+      .fadeOut();
+  });
+  $('.js-remove-tr').bind('ajax:before', function () {
+    return $(this).hide();
+  });
+  $('.js-remove-tr').bind('ajax:success', function () {
+    return $(this).closest('tr').fadeOut();
+  });
+  $('select.select2').select2({
+    width: 'resolve',
+    // Initialize select2 selects
+    dropdownAutoWidth: true
+  });
+  $('.js-select2').bind('select2-close', function () {
+    return setTimeout((function () {
+      $('.select2-container-active').removeClass('select2-container-active');
+      return $(':focus').blur();
+    }), 1);
+  // Close select2 on escape
+  });
+  // Initialize tooltips
+  $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
+  $body.tooltip({
+    selector: '.has-tooltip, [data-toggle="tooltip"]',
+    placement: function (tip, el) {
+      return $(el).data('placement') || 'bottom';
+    }
+  });
+  $('.trigger-submit').on('change', function () {
+    return $(this).parents('form').submit();
+  // Form submitter
+  });
+  gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+  // Flash
+  if ($flash.length > 0) {
+    $flash.click(function () {
+      return $(this).fadeOut();
+    });
+    $flash.show();
+  }
+  // Disable form buttons while a form is submitting
+  $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
+    var buttons;
+    buttons = $('[type="submit"]', this);
+    switch (e.type) {
+      case 'ajax:beforeSend':
+      case 'submit':
+        return buttons.disable();
+      default:
+        return buttons.enable();
+    }
+  });
+  $(document).ajaxError(function (e, xhrObj) {
+    var ref = xhrObj.status;
+    if (xhrObj.status === 401) {
+      return new Flash('You need to be logged in.', 'alert');
+    } else if (ref === 404 || ref === 500) {
+      return new Flash('Something went wrong on our end.', 'alert');
+    }
+  });
+  $('.account-box').hover(function () {
+    // Show/Hide the profile menu when hovering the account box
+    return $(this).toggleClass('hover');
+  });
+  $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
+    var $container;
+    $container = $(this).parent();
+    $container.next('table').show();
+    return $container.remove();
+  // Commit show suppressed diff
+  });
+  $('.navbar-toggle').on('click', function () {
+    $('.header-content .title').toggle();
+    $('.header-content .header-logo').toggle();
+    $('.header-content .navbar-collapse').toggle();
+    return $('.navbar-toggle').toggleClass('active');
+  });
+  // Show/hide comments on diff
+  $body.on('click', '.js-toggle-diff-comments', function (e) {
+    var $this = $(this);
+    var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+    $this.toggleClass('active');
+    if ($this.hasClass('active')) {
+      notesHolders.show().find('.hide, .content').show();
+    } else {
+      notesHolders.hide().find('.content').hide();
+    }
+    $(document).trigger('toggle.comments');
+    return e.preventDefault();
+  });
+  $document.off('click', '.js-confirm-danger');
+  $document.on('click', '.js-confirm-danger', function (e) {
+    var btn = $(e.target);
+    var form = btn.closest('form');
+    var text = btn.data('confirm-danger-message');
+    e.preventDefault();
+    return new ConfirmDangerModal(form, text);
+  });
+  $('input[type="search"]').each(function () {
+    var $this = $(this);
+    $this.attr('value', $this.val());
+  });
+  $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
+    var $this;
+    $this = $(this);
+    return $this.attr('value', $this.val());
+  });
+  $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
+    var $gutterIcon;
+    if (breakpoint === 'sm' || breakpoint === 'xs') {
+      $gutterIcon = $sidebarGutterToggle.find('i');
+      if ($gutterIcon.hasClass('fa-angle-double-right')) {
+        return $sidebarGutterToggle.trigger('click');
+      }
+    }
+  });
+  fitSidebarForSize = function () {
+    var oldBootstrapBreakpoint;
+    oldBootstrapBreakpoint = bootstrapBreakpoint;
+    bootstrapBreakpoint = bp.getBreakpointSize();
+    if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
+      return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+    }
+  };
+  $window.off('resize.app').on('resize.app', function () {
+    return fitSidebarForSize();
+  });
+  gl.awardsHandler = new AwardsHandler();
+  new Aside();
+
+  gl.utils.initTimeagoTimeout();
+});
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js
similarity index 100%
rename from app/assets/javascripts/member_expiration_date.js.es6
rename to app/assets/javascripts/member_expiration_date.js
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js
similarity index 100%
rename from app/assets/javascripts/members.js.es6
rename to app/assets/javascripts/members.js
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
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
rename to app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
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
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
rename to app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
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
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
rename to app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
rename to app/assets/javascripts/merge_conflicts/merge_conflict_service.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
rename to app/assets/javascripts/merge_conflicts/merge_conflict_store.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
rename to app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
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
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
rename to app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
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
similarity index 100%
rename from app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
rename to app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js
similarity index 100%
rename from app/assets/javascripts/merge_request_tabs.js.es6
rename to app/assets/javascripts/merge_request_tabs.js
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js
similarity index 86%
rename from app/assets/javascripts/merge_request_widget.js.es6
rename to app/assets/javascripts/merge_request_widget.js
index 88f08bbaa345ee6cafdf524a569a64308b31a804..0e2af3df071d53c6094e885398876df425af9e49 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -3,7 +3,8 @@
 /* global notifyPermissions */
 /* global merge_request_widget */
 
-require('./smart_interval');
+import './smart_interval';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
 
 ((global) => {
   var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -13,13 +14,13 @@ require('./smart_interval');
          <%= ci_success_icon %>
          <span>
            Deployed to
-           <a href="<%- url %>" target="_blank" class="environment">
+           <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
              <%- name %>
            </a>
            <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
              <%- deployed_at %>
            </span>
-           <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+           <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
              <i class="fa fa-external-link"></i>
              View on <%- external_url_formatted %>
            </a>
@@ -83,7 +84,7 @@ require('./smart_interval');
         return function() {
           var page;
           page = $('body').data('page').split(':').last();
-          if (allowedPages.indexOf(page) < 0) {
+          if (allowedPages.indexOf(page) === -1) {
             return _this.clearEventListeners();
           }
         };
@@ -129,8 +130,9 @@ require('./smart_interval');
     };
 
     MergeRequestWidget.prototype.getMergeStatus = function() {
-      return $.get(this.opts.merge_check_url, function(data) {
+      return $.get(this.opts.merge_check_url, (data) => {
         var $html = $(data);
+        this.updateMergeButton(this.status, this.hasCi, $html);
         $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
         $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
       });
@@ -154,9 +156,9 @@ require('./smart_interval');
       return $.getJSON(this.opts.ci_status_url, (function(_this) {
         return function(data) {
           var message, status, title;
-          if (!data.status) {
-            return;
-          }
+          _this.status = data.status;
+          _this.hasCi = data.has_ci;
+          _this.updateMergeButton(_this.status, _this.hasCi);
           if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
           if (data.status !== _this.opts.ci_status ||
               data.sha !== _this.opts.ci_sha ||
@@ -174,7 +176,7 @@ require('./smart_interval');
               _this.opts.ci_sha = data.sha;
               _this.updateCommitUrls(data.sha);
             }
-            if (showNotification) {
+            if (showNotification && data.status) {
               status = _this.ciLabelForStatus(data.status);
               if (status === "preparing") {
                 title = _this.opts.ci_title.preparing;
@@ -232,36 +234,45 @@ require('./smart_interval');
         return;
       }
       $('.ci_widget').hide();
-      allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
-      if (indexOf.call(allowed_states, state) >= 0) {
-        $('.ci_widget.ci-' + state).show();
+      $('.ci_widget.ci-' + state).show();
+
+      this.initMiniPipelineGraph();
+    };
+
+    MergeRequestWidget.prototype.showCICoverage = function(coverage) {
+      var text = `Coverage ${coverage}%`;
+      return $('.ci_widget:visible .ci-coverage').text(text);
+    };
+
+    MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) {
+      const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
+      let stateClass = 'btn-danger';
+      if (!hasCi) {
+        stateClass = 'btn-create';
+      } else if (indexOf.call(allowed_states, state) !== -1) {
         switch (state) {
           case "failed":
           case "canceled":
           case "not_found":
-            this.setMergeButtonClass('btn-danger');
+            stateClass = 'btn-danger';
             break;
           case "running":
-            this.setMergeButtonClass('btn-info');
+            stateClass = 'btn-info';
             break;
           case "success":
           case "success_with_warnings":
-            this.setMergeButtonClass('btn-create');
+            stateClass = 'btn-create';
         }
       } else {
         $('.ci_widget.ci-error').show();
-        this.setMergeButtonClass('btn-danger');
+        stateClass = 'btn-danger';
       }
-    };
 
-    MergeRequestWidget.prototype.showCICoverage = function(coverage) {
-      var text;
-      text = 'Coverage ' + coverage + '%';
-      return $('.ci_widget:visible .ci-coverage').text(text);
+      this.setMergeButtonClass(stateClass, $html);
     };
 
-    MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) {
-      return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
+    MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) {
+      return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class);
     };
 
     MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
@@ -275,7 +286,7 @@ require('./smart_interval');
     };
 
     MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
-      new gl.MiniPipelineGraph({
+      new MiniPipelineGraph({
         container: '.js-pipeline-inline-mr-widget-graph:visible',
       }).bindEvents();
     };
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js
similarity index 77%
rename from app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
rename to app/assets/javascripts/merge_request_widget/ci_bundle.js
index 5840916846b87e1b5ae9ba9961ad47e1492eb978..21d7c3e168ef0c81b4f7d82daee188a34eb5cde9 100644
--- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js
@@ -15,15 +15,15 @@
     });
 
     $(document)
-    .off('click', '.accept_merge_request')
-    .on('click', '.accept_merge_request', () => {
-      $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+    .off('click', '.accept-merge-request')
+    .on('click', '.accept-merge-request', () => {
+      $('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
     });
 
     $(document)
-    .off('click', '.merge_when_build_succeeds')
-    .on('click', '.merge_when_build_succeeds', () => {
-      $('#merge_when_build_succeeds').val('1');
+    .off('click', '.merge-when-pipeline-succeeds')
+    .on('click', '.merge-when-pipeline-succeeds', () => {
+      $('#merge_when_pipeline_succeeds').val('1');
     });
 
     $(document)
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7fbaeec7882c0d4922256924b272b5dd1ee80f9e..38c673e890782b89cc376cd21a9d34c81389281d 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -78,7 +78,6 @@
       } else {
         $(element).find('.assignee-icon').empty();
       }
-      return $(element).effect('highlight');
     };
 
     function Milestone() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 8df1c8e7f9463ca016c3368cfc90e71db5956694..40e977df693e8b9dc098b1f79cc0a32d8f7d9f15 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -19,7 +19,7 @@
       }
 
       $els.each(function(i, dropdown) {
-        var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
+        var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
         $dropdown = $(dropdown);
         projectId = $dropdown.data('project-id');
         milestonesUrl = $dropdown.data('milestones');
@@ -29,6 +29,7 @@
         showAny = $dropdown.data('show-any');
         showMenuAbove = $dropdown.data('showMenuAbove');
         showUpcoming = $dropdown.data('show-upcoming');
+        showStarted = $dropdown.data('show-started');
         useId = $dropdown.data('use-id');
         defaultLabel = $dropdown.data('default-label');
         issuableId = $dropdown.data('issuable-id');
@@ -39,7 +40,7 @@
         $value = $block.find('.value');
         $loading = $block.find('.block-loading').fadeOut();
         if (issueUpdateURL) {
-          milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+          milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
           milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
           collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
         }
@@ -71,6 +72,13 @@
                   title: 'Upcoming'
                 });
               }
+              if (showStarted) {
+                extraOptions.push({
+                  id: -3,
+                  name: '#started',
+                  title: 'Started'
+                });
+              }
               if (extraOptions.length) {
                 extraOptions.push('divider');
               }
@@ -124,18 +132,12 @@
               return;
             }
 
-            if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
-              !$dropdown.closest('.add-issues-modal').length) {
-              boardsStore = gl.issueBoards.BoardsStore.state.filters;
-            } else if ($dropdown.closest('.add-issues-modal').length) {
+            if ($dropdown.closest('.add-issues-modal').length) {
               boardsStore = gl.issueBoards.ModalStore.store.filter;
             }
 
             if (boardsStore) {
               boardsStore[$dropdown.data('field-name')] = selected.name;
-              if (!$dropdown.closest('.add-issues-modal').length) {
-                gl.issueBoards.BoardsStore.updateFiltersUrl();
-              }
               e.preventDefault();
             } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
               if (selected.name != null) {
@@ -157,7 +159,7 @@
               }
 
               $dropdown.trigger('loading.gl.dropdown');
-              $loading.fadeIn();
+              $loading.removeClass('hidden').fadeIn();
 
               gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
                 .then(function () {
@@ -169,7 +171,7 @@
               data = {};
               data[abilityName] = {};
               data[abilityName].milestone_id = selected != null ? selected : null;
-              $loading.fadeIn();
+              $loading.removeClass('hidden').fadeIn();
               $dropdown.trigger('loading.gl.dropdown');
               return $.ajax({
                 type: 'PUT',
@@ -181,8 +183,7 @@
                 $selectbox.hide();
                 $value.css('display', '');
                 if (data.milestone != null) {
-                  data.milestone.namespace = _this.currentProject.namespace;
-                  data.milestone.path = _this.currentProject.path;
+                  data.milestone.full_path = _this.currentProject.full_path;
                   data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
                   $value.html(milestoneLinkTemplate(data.milestone));
                   return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c58c4650013b6fb830bd808d05edeff15c29104
--- /dev/null
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -0,0 +1,110 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+/**
+ * In each pipelines table we have a mini pipeline graph for each pipeline.
+ *
+ * When we click in a pipeline stage, we need to make an API call to get the
+ * builds list to render in a dropdown.
+ *
+ * The container should be the table element.
+ *
+ * The stage icon clicked needs to have the following HTML structure:
+ * <div class="dropdown">
+ *   <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
+ *   <div class="js-builds-dropdown-container dropdown-menu"></div>
+ * </div>
+ */
+
+export default class MiniPipelineGraph {
+  constructor(opts = {}) {
+    this.container = opts.container || '';
+    this.dropdownListSelector = '.js-builds-dropdown-container';
+    this.getBuildsList = this.getBuildsList.bind(this);
+  }
+
+  /**
+   * Adds the event listener when the dropdown is opened.
+   * All dropdown events are fired at the .dropdown-menu's parent element.
+   */
+  bindEvents() {
+    $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
+  }
+
+  /**
+   * When the user right clicks or cmd/ctrl + click in the job name
+   * the dropdown should not be closed and the link should open in another tab,
+   * so we stop propagation of the click event inside the dropdown.
+   *
+   * Since this component is rendered multiple times per page we need to guarantee we only
+   * target the click event of this component.
+   */
+  stopDropdownClickPropagation() {
+    $(document).on(
+      'click',
+      `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`,
+      (e) => {
+        e.stopPropagation();
+      },
+    );
+  }
+
+  /**
+   * For the clicked stage, renders the given data in the dropdown list.
+   *
+   * @param  {HTMLElement} stageContainer
+   * @param  {Object} data
+   */
+  renderBuildsList(stageContainer, data) {
+    const dropdownContainer = stageContainer.parentElement.querySelector(
+      `${this.dropdownListSelector} .js-builds-dropdown-list`,
+    );
+
+    dropdownContainer.innerHTML = data;
+  }
+
+  /**
+   * For the clicked stage, gets the list of builds.
+   *
+   * All dropdown events have a relatedTarget property,
+   * whose value is the toggling anchor element.
+   *
+   * @param  {Object} e bootstrap dropdown event
+   * @return {Promise}
+   */
+  getBuildsList(e) {
+    const button = e.relatedTarget;
+    const endpoint = button.dataset.stageEndpoint;
+
+    return $.ajax({
+      dataType: 'json',
+      type: 'GET',
+      url: endpoint,
+      beforeSend: () => {
+        this.renderBuildsList(button, '');
+        this.toggleLoading(button);
+      },
+      success: (data) => {
+        this.toggleLoading(button);
+        this.renderBuildsList(button, data.html);
+        this.stopDropdownClickPropagation();
+      },
+      error: () => {
+        this.toggleLoading(button);
+        new Flash('An error occurred while fetching the builds.', 'alert');
+      },
+    });
+  }
+
+  /**
+   * Toggles the visibility of the loading icon.
+   *
+   * @param  {HTMLElement} stageContainer
+   * @return {type}
+   */
+  toggleLoading(stageContainer) {
+    stageContainer.parentElement.querySelector(
+      `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+    ).classList.toggle('hidden');
+  }
+}
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
deleted file mode 100644
index 2145e531331daa00c2405a68f8cda7ef355f4079..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ /dev/null
@@ -1,95 +0,0 @@
-/* eslint-disable no-new */
-/* global Flash */
-
-/**
- * In each pipelines table we have a mini pipeline graph for each pipeline.
- *
- * When we click in a pipeline stage, we need to make an API call to get the
- * builds list to render in a dropdown.
- *
- * The container should be the table element.
- *
- * The stage icon clicked needs to have the following HTML structure:
- * <div class="dropdown">
- *   <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
- *   <div class="js-builds-dropdown-container dropdown-menu"></div>
- * </div>
- */
-(() => {
-  class MiniPipelineGraph {
-    constructor(opts = {}) {
-      this.container = opts.container || '';
-      this.dropdownListSelector = '.js-builds-dropdown-container';
-      this.getBuildsList = this.getBuildsList.bind(this);
-    }
-
-    /**
-     * Adds the event listener when the dropdown is opened.
-     * All dropdown events are fired at the .dropdown-menu's parent element.
-     */
-    bindEvents() {
-      $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
-    }
-
-    /**
-     * For the clicked stage, renders the given data in the dropdown list.
-     *
-     * @param  {HTMLElement} stageContainer
-     * @param  {Object} data
-     */
-    renderBuildsList(stageContainer, data) {
-      const dropdownContainer = stageContainer.parentElement.querySelector(
-        `${this.dropdownListSelector} .js-builds-dropdown-list`,
-      );
-
-      dropdownContainer.innerHTML = data;
-    }
-
-    /**
-     * For the clicked stage, gets the list of builds.
-     *
-     * All dropdown events have a relatedTarget property,
-     * whose value is the toggling anchor element.
-     *
-     * @param  {Object} e bootstrap dropdown event
-     * @return {Promise}
-     */
-    getBuildsList(e) {
-      const button = e.relatedTarget;
-      const endpoint = button.dataset.stageEndpoint;
-
-      return $.ajax({
-        dataType: 'json',
-        type: 'GET',
-        url: endpoint,
-        beforeSend: () => {
-          this.renderBuildsList(button, '');
-          this.toggleLoading(button);
-        },
-        success: (data) => {
-          this.toggleLoading(button);
-          this.renderBuildsList(button, data.html);
-        },
-        error: () => {
-          this.toggleLoading(button);
-          new Flash('An error occurred while fetching the builds.', 'alert');
-        },
-      });
-    }
-
-    /**
-     * Toggles the visibility of the loading icon.
-     *
-     * @param  {HTMLElement} stageContainer
-     * @return {type}
-     */
-    toggleLoading(stageContainer) {
-      stageContainer.parentElement.querySelector(
-        `${this.dropdownListSelector} .js-builds-dropdown-loading`,
-      ).classList.toggle('hidden');
-    }
-  }
-
-  window.gl = window.gl || {};
-  window.gl.MiniPipelineGraph = MiniPipelineGraph;
-})();
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3ce931041767f1dcacb761e36a13bc0afbe6196
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+  document.removeEventListener('DOMContentLoaded', onLoad, false);
+  return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
new file mode 100644
index 0000000000000000000000000000000000000000..fcffc11a2df0e8b411f54d370724f52c023d0133
--- /dev/null
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -0,0 +1,335 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+import d3 from 'd3';
+import statusCodes from '~/lib/utils/http_status';
+import '../lib/utils/common_utils';
+import '../flash';
+
+const prometheusGraphsContainer = '.prometheus-graph';
+const metricsEndpoint = 'metrics.json';
+const timeFormat = d3.time.format('%H:%M');
+const dayFormat = d3.time.format('%b %e, %a');
+const bisectDate = d3.bisector(d => d.time).left;
+const extraAddedWidthParent = 100;
+
+class PrometheusGraph {
+
+  constructor() {
+    this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
+    this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
+    const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
+    extraAddedWidthParent;
+    this.originalWidth = parentContainerWidth;
+    this.originalHeight = 400;
+    this.width = parentContainerWidth - this.margin.left - this.margin.right;
+    this.height = 400 - this.margin.top - this.margin.bottom;
+    this.backOffRequestCounter = 0;
+    this.configureGraph();
+    this.init();
+  }
+
+  createGraph() {
+    Object.keys(this.data).forEach((key) => {
+      const value = this.data[key];
+      if (value.length > 0) {
+        this.plotValues(value, key);
+      }
+    });
+  }
+
+  init() {
+    this.getData().then((metricsResponse) => {
+      if (Object.keys(metricsResponse).length === 0) {
+        new Flash('Empty metrics', 'alert');
+      } else {
+        this.transformData(metricsResponse);
+        this.createGraph();
+      }
+    });
+  }
+
+  plotValues(valuesToPlot, key) {
+    const x = d3.time.scale()
+        .range([0, this.width]);
+
+    const y = d3.scale.linear()
+        .range([this.height, 0]);
+
+    const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
+
+    const graphSpecifics = this.graphSpecificProperties[key];
+
+    const chart = d3.select(prometheusGraphContainer)
+        .attr('width', this.width + this.margin.left + this.margin.right)
+        .attr('height', this.height + this.margin.bottom + this.margin.top)
+        .append('g')
+          .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
+
+    const axisLabelContainer = d3.select(prometheusGraphContainer)
+      .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
+      .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
+      .append('g')
+        .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
+
+    x.domain(d3.extent(valuesToPlot, d => d.time));
+    y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
+
+    const xAxis = d3.svg.axis()
+        .scale(x)
+        .ticks(this.commonGraphProperties.axis_no_ticks)
+        .orient('bottom');
+
+    const yAxis = d3.svg.axis()
+        .scale(y)
+        .ticks(this.commonGraphProperties.axis_no_ticks)
+        .tickSize(-this.width)
+        .orient('left');
+
+    this.createAxisLabelContainers(axisLabelContainer, key);
+
+    chart.append('g')
+        .attr('class', 'x-axis')
+        .attr('transform', `translate(0,${this.height})`)
+        .call(xAxis);
+
+    chart.append('g')
+        .attr('class', 'y-axis')
+        .call(yAxis);
+
+    const area = d3.svg.area()
+      .x(d => x(d.time))
+      .y0(this.height)
+      .y1(d => y(d.value))
+      .interpolate('linear');
+
+    const line = d3.svg.line()
+    .x(d => x(d.time))
+    .y(d => y(d.value));
+
+    chart.append('path')
+    .datum(valuesToPlot)
+    .attr('d', area)
+    .attr('class', 'metric-area')
+    .attr('fill', graphSpecifics.area_fill_color);
+
+    chart.append('path')
+      .datum(valuesToPlot)
+      .attr('class', 'metric-line')
+      .attr('stroke', graphSpecifics.line_color)
+      .attr('fill', 'none')
+      .attr('stroke-width', this.commonGraphProperties.area_stroke_width)
+      .attr('d', line);
+
+    // Overlay area for the mouseover events
+    chart.append('rect')
+      .attr('class', 'prometheus-graph-overlay')
+      .attr('width', this.width)
+      .attr('height', this.height)
+      .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
+  }
+
+  // The legends from the metric
+  createAxisLabelContainers(axisLabelContainer, key) {
+    const graphSpecifics = this.graphSpecificProperties[key];
+
+    axisLabelContainer.append('line')
+        .attr('class', 'label-x-axis-line')
+        .attr('stroke', '#000000')
+        .attr('stroke-width', '1')
+        .attr({
+          x1: 0,
+          y1: this.originalHeight - this.marginLabelContainer.top,
+          x2: this.originalWidth - this.margin.right,
+          y2: this.originalHeight - this.marginLabelContainer.top,
+        });
+
+    axisLabelContainer.append('line')
+          .attr('class', 'label-y-axis-line')
+          .attr('stroke', '#000000')
+          .attr('stroke-width', '1')
+          .attr({
+            x1: 0,
+            y1: 0,
+            x2: 0,
+            y2: this.originalHeight - this.marginLabelContainer.top,
+          });
+
+    axisLabelContainer.append('text')
+          .attr('class', 'label-axis-text')
+          .attr('text-anchor', 'middle')
+          .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
+          .text(graphSpecifics.graph_legend_title);
+
+    axisLabelContainer.append('rect')
+          .attr('class', 'rect-axis-text')
+          .attr('x', (this.originalWidth / 2) - this.margin.right)
+          .attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
+          .attr('width', 30)
+          .attr('height', 80);
+
+    axisLabelContainer.append('text')
+          .attr('class', 'label-axis-text')
+          .attr('x', (this.originalWidth / 2) - this.margin.right)
+          .attr('y', this.originalHeight - this.marginLabelContainer.top)
+          .attr('dy', '.35em')
+          .text('Time');
+
+    // Legends
+
+    // Metric Usage
+    axisLabelContainer.append('rect')
+          .attr('x', this.originalWidth - 170)
+          .attr('y', (this.originalHeight / 2) - 80)
+          .style('fill', graphSpecifics.area_fill_color)
+          .attr('width', 20)
+          .attr('height', 35);
+
+    axisLabelContainer.append('text')
+          .attr('class', 'label-axis-text')
+          .attr('x', this.originalWidth - 140)
+          .attr('y', (this.originalHeight / 2) - 65)
+          .text(graphSpecifics.graph_legend_title);
+
+    axisLabelContainer.append('text')
+            .attr('class', 'text-metric-usage')
+            .attr('x', this.originalWidth - 140)
+            .attr('y', (this.originalHeight / 2) - 50);
+  }
+
+  handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
+    const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
+    const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
+    const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
+    const d0 = valuesToPlot[timeValueIndex - 1];
+    const d1 = valuesToPlot[timeValueIndex];
+    const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
+    const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
+    const currentTimeCoordinate = x(currentData.time);
+    const graphSpecifics = this.graphSpecificProperties[key];
+    // Remove the current selectors
+    d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
+    d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
+    d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
+    d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
+
+    chart.append('line')
+    .attr('class', 'selected-metric-line')
+    .attr({
+      x1: currentTimeCoordinate,
+      y1: y(0),
+      x2: currentTimeCoordinate,
+      y2: maxValueMetric,
+    });
+
+    chart.append('circle')
+    .attr('class', 'circle-metric')
+    .attr('fill', graphSpecifics.line_color)
+    .attr('cx', currentTimeCoordinate)
+    .attr('cy', y(currentData.value))
+    .attr('r', this.commonGraphProperties.circle_radius_metric);
+
+    // The little box with text
+    const rectTextMetric = chart.append('g')
+    .attr('class', 'rect-text-metric')
+    .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
+
+    rectTextMetric.append('rect')
+    .attr('class', 'rect-metric')
+    .attr('x', currentTimeCoordinate + 10)
+    .attr('y', maxValueMetric)
+    .attr('width', this.commonGraphProperties.rect_text_width)
+    .attr('height', this.commonGraphProperties.rect_text_height);
+
+    rectTextMetric.append('text')
+    .attr('class', 'text-metric')
+    .attr('x', currentTimeCoordinate + 35)
+    .attr('y', maxValueMetric + 35)
+    .text(timeFormat(currentData.time));
+
+    rectTextMetric.append('text')
+    .attr('class', 'text-metric-date')
+    .attr('x', currentTimeCoordinate + 15)
+    .attr('y', maxValueMetric + 15)
+    .text(dayFormat(currentData.time));
+
+    // Update the text
+    d3.select(`${prometheusGraphContainer} .text-metric-usage`)
+      .text(currentData.value.substring(0, 8));
+  }
+
+  configureGraph() {
+    this.graphSpecificProperties = {
+      cpu_values: {
+        area_fill_color: '#edf3fc',
+        line_color: '#5b99f7',
+        graph_legend_title: 'CPU Usage (Cores)',
+      },
+      memory_values: {
+        area_fill_color: '#fca326',
+        line_color: '#fc6d26',
+        graph_legend_title: 'Memory Usage (MB)',
+      },
+    };
+
+    this.commonGraphProperties = {
+      area_stroke_width: 2,
+      median_total_characters: 8,
+      circle_radius_metric: 5,
+      rect_text_width: 90,
+      rect_text_height: 40,
+      axis_no_ticks: 3,
+    };
+  }
+
+  getData() {
+    const maxNumberOfRequests = 3;
+    return gl.utils.backOff((next, stop) => {
+      $.ajax({
+        url: metricsEndpoint,
+        dataType: 'json',
+      })
+      .done((data, statusText, resp) => {
+        if (resp.status === statusCodes.NO_CONTENT) {
+          this.backOffRequestCounter = this.backOffRequestCounter += 1;
+          if (this.backOffRequestCounter < maxNumberOfRequests) {
+            next();
+          } else {
+            stop({
+              status: resp.status,
+              metrics: data,
+            });
+          }
+        } else {
+          stop({
+            status: resp.status,
+            metrics: data,
+          });
+        }
+      }).fail(stop);
+    })
+    .then((resp) => {
+      if (resp.status === statusCodes.NO_CONTENT) {
+        return {};
+      }
+      return resp.metrics;
+    })
+    .catch(() => new Flash('An error occurred while fetching metrics.', 'alert'));
+  }
+
+  transformData(metricsResponse) {
+    const metricTypes = {};
+    Object.keys(metricsResponse.metrics).forEach((key) => {
+      if (key === 'cpu_values' || key === 'memory_values') {
+        const metricValues = (metricsResponse.metrics[key])[0];
+        metricTypes[key] = metricValues.values.map(metric => ({
+          time: new Date(metric[0] * 1000),
+          value: metric[1],
+        }));
+      }
+    });
+    this.data = metricTypes;
+  }
+}
+
+export default PrometheusGraph;
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 43dc9838977772245f1b93a6b1127868cf419b66..5aad3908eb6595dbd1645b95e86c0957afd02b03 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,424 +1,347 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
-/* global Raphael */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
 
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+import Raphael from './raphael';
 
-  this.BranchGraph = (function() {
-    function BranchGraph(element1, options1) {
-      this.element = element1;
-      this.options = options1;
-      this.scrollTop = bind(this.scrollTop, this);
-      this.scrollBottom = bind(this.scrollBottom, this);
-      this.scrollRight = bind(this.scrollRight, this);
-      this.scrollLeft = bind(this.scrollLeft, this);
-      this.scrollUp = bind(this.scrollUp, this);
-      this.scrollDown = bind(this.scrollDown, this);
-      this.preparedCommits = {};
-      this.mtime = 0;
-      this.mspace = 0;
-      this.parents = {};
-      this.colors = ["#000"];
-      this.offsetX = 150;
-      this.offsetY = 20;
-      this.unitTime = 30;
-      this.unitSpace = 10;
-      this.prev_start = -1;
-      this.load();
-    }
-
-    BranchGraph.prototype.load = function() {
-      return $.ajax({
-        url: this.options.url,
-        method: "get",
-        dataType: "json",
-        success: $.proxy(function(data) {
-          $(".loading", this.element).hide();
-          this.prepareData(data.days, data.commits);
-          return this.buildGraph();
-        }, this)
-      });
-    };
+export default (function() {
+  function BranchGraph(element1, options1) {
+    this.element = element1;
+    this.options = options1;
+    this.scrollTop = this.scrollTop.bind(this);
+    this.scrollBottom = this.scrollBottom.bind(this);
+    this.scrollRight = this.scrollRight.bind(this);
+    this.scrollLeft = this.scrollLeft.bind(this);
+    this.scrollUp = this.scrollUp.bind(this);
+    this.scrollDown = this.scrollDown.bind(this);
+    this.preparedCommits = {};
+    this.mtime = 0;
+    this.mspace = 0;
+    this.parents = {};
+    this.colors = ["#000"];
+    this.offsetX = 150;
+    this.offsetY = 20;
+    this.unitTime = 30;
+    this.unitSpace = 10;
+    this.prev_start = -1;
+    this.load();
+  }
 
-    BranchGraph.prototype.prepareData = function(days, commits) {
-      var c, ch, cw, j, len, ref;
-      this.days = days;
-      this.commits = commits;
-      this.collectParents();
-      this.graphHeight = $(this.element).height();
-      this.graphWidth = $(this.element).width();
-      ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
-      cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
-      this.r = Raphael(this.element.get(0), cw, ch);
-      this.top = this.r.set();
-      this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
-      ref = this.commits;
-      for (j = 0, len = ref.length; j < len; j += 1) {
-        c = ref[j];
-        if (c.id in this.parents) {
-          c.isParent = true;
-        }
-        this.preparedCommits[c.id] = c;
-        this.markCommit(c);
-      }
-      return this.collectColors();
-    };
-
-    BranchGraph.prototype.collectParents = function() {
-      var c, j, len, p, ref, results;
-      ref = this.commits;
-      results = [];
-      for (j = 0, len = ref.length; j < len; j += 1) {
-        c = ref[j];
-        this.mtime = Math.max(this.mtime, c.time);
-        this.mspace = Math.max(this.mspace, c.space);
-        results.push((function() {
-          var l, len1, ref1, results1;
-          ref1 = c.parents;
-          results1 = [];
-          for (l = 0, len1 = ref1.length; l < len1; l += 1) {
-            p = ref1[l];
-            this.parents[p[0]] = true;
-            results1.push(this.mspace = Math.max(this.mspace, p[1]));
-          }
-          return results1;
-        }).call(this));
-      }
-      return results;
-    };
+  BranchGraph.prototype.load = function() {
+    return $.ajax({
+      url: this.options.url,
+      method: "get",
+      dataType: "json",
+      success: $.proxy(function(data) {
+        $(".loading", this.element).hide();
+        this.prepareData(data.days, data.commits);
+        return this.buildGraph();
+      }, this)
+    });
+  };
 
-    BranchGraph.prototype.collectColors = function() {
-      var k, results;
-      k = 0;
-      results = [];
-      while (k < this.mspace) {
-        this.colors.push(Raphael.getColor(.8));
-        // Skipping a few colors in the spectrum to get more contrast between colors
-        Raphael.getColor();
-        Raphael.getColor();
-        results.push(k += 1);
+  BranchGraph.prototype.prepareData = function(days, commits) {
+    var c, ch, cw, j, len, ref;
+    this.days = days;
+    this.commits = commits;
+    this.collectParents();
+    this.graphHeight = $(this.element).height();
+    this.graphWidth = $(this.element).width();
+    ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
+    cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
+    this.r = Raphael(this.element.get(0), cw, ch);
+    this.top = this.r.set();
+    this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
+    ref = this.commits;
+    for (j = 0, len = ref.length; j < len; j += 1) {
+      c = ref[j];
+      if (c.id in this.parents) {
+        c.isParent = true;
       }
-      return results;
-    };
+      this.preparedCommits[c.id] = c;
+      this.markCommit(c);
+    }
+    return this.collectColors();
+  };
 
-    BranchGraph.prototype.buildGraph = function() {
-      var cuday, cumonth, day, j, len, mm, r, ref;
-      r = this.r;
-      cuday = 0;
-      cumonth = "";
-      r.rect(0, 0, 40, this.barHeight).attr({
-        fill: "#222"
-      });
-      r.rect(40, 0, 30, this.barHeight).attr({
-        fill: "#444"
-      });
-      ref = this.days;
-      for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
-        day = ref[mm];
-        if (cuday !== day[0] || cumonth !== day[1]) {
-          // Dates
-          r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
-            font: "12px Monaco, monospace",
-            fill: "#BBB"
-          });
-          cuday = day[0];
-        }
-        if (cumonth !== day[1]) {
-          // Months
-          r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
-            font: "12px Monaco, monospace",
-            fill: "#EEE"
-          });
-          cumonth = day[1];
+  BranchGraph.prototype.collectParents = function() {
+    var c, j, len, p, ref, results;
+    ref = this.commits;
+    results = [];
+    for (j = 0, len = ref.length; j < len; j += 1) {
+      c = ref[j];
+      this.mtime = Math.max(this.mtime, c.time);
+      this.mspace = Math.max(this.mspace, c.space);
+      results.push((function() {
+        var l, len1, ref1, results1;
+        ref1 = c.parents;
+        results1 = [];
+        for (l = 0, len1 = ref1.length; l < len1; l += 1) {
+          p = ref1[l];
+          this.parents[p[0]] = true;
+          results1.push(this.mspace = Math.max(this.mspace, p[1]));
         }
-      }
-      this.renderPartialGraph();
-      return this.bindEvents();
-    };
+        return results1;
+      }).call(this));
+    }
+    return results;
+  };
 
-    BranchGraph.prototype.renderPartialGraph = function() {
-      var commit, end, i, isGraphEdge, start, x, y;
-      start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
-      if (start < 0) {
-        isGraphEdge = true;
-        start = 0;
+  BranchGraph.prototype.collectColors = function() {
+    var k, results;
+    k = 0;
+    results = [];
+    while (k < this.mspace) {
+      this.colors.push(Raphael.getColor(.8));
+      // Skipping a few colors in the spectrum to get more contrast between colors
+      Raphael.getColor();
+      Raphael.getColor();
+      results.push(k += 1);
+    }
+    return results;
+  };
+
+  BranchGraph.prototype.buildGraph = function() {
+    var cuday, cumonth, day, j, len, mm, r, ref;
+    r = this.r;
+    cuday = 0;
+    cumonth = "";
+    r.rect(0, 0, 40, this.barHeight).attr({
+      fill: "#222"
+    });
+    r.rect(40, 0, 30, this.barHeight).attr({
+      fill: "#444"
+    });
+    ref = this.days;
+    for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+      day = ref[mm];
+      if (cuday !== day[0] || cumonth !== day[1]) {
+        // Dates
+        r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
+          font: "12px Monaco, monospace",
+          fill: "#BBB"
+        });
+        cuday = day[0];
       }
-      end = start + 40;
-      if (this.commits.length < end) {
-        isGraphEdge = true;
-        end = this.commits.length;
+      if (cumonth !== day[1]) {
+        // Months
+        r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
+          font: "12px Monaco, monospace",
+          fill: "#EEE"
+        });
+        cumonth = day[1];
       }
-      if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
-        i = start;
-        this.prev_start = start;
-        while (i < end) {
-          commit = this.commits[i];
-          i += 1;
-          if (commit.hasDrawn !== true) {
-            x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
-            y = this.offsetY + this.unitTime * commit.time;
-            this.drawDot(x, y, commit);
-            this.drawLines(x, y, commit);
-            this.appendLabel(x, y, commit);
-            this.appendAnchor(x, y, commit);
-            commit.hasDrawn = true;
-          }
+    }
+    this.renderPartialGraph();
+    return this.bindEvents();
+  };
+
+  BranchGraph.prototype.renderPartialGraph = function() {
+    var commit, end, i, isGraphEdge, start, x, y;
+    start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
+    if (start < 0) {
+      isGraphEdge = true;
+      start = 0;
+    }
+    end = start + 40;
+    if (this.commits.length < end) {
+      isGraphEdge = true;
+      end = this.commits.length;
+    }
+    if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
+      i = start;
+      this.prev_start = start;
+      while (i < end) {
+        commit = this.commits[i];
+        i += 1;
+        if (commit.hasDrawn !== true) {
+          x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+          y = this.offsetY + this.unitTime * commit.time;
+          this.drawDot(x, y, commit);
+          this.drawLines(x, y, commit);
+          this.appendLabel(x, y, commit);
+          this.appendAnchor(x, y, commit);
+          commit.hasDrawn = true;
         }
-        return this.top.toFront();
       }
-    };
-
-    BranchGraph.prototype.bindEvents = function() {
-      var element;
-      element = this.element;
-      return $(element).scroll((function(_this) {
-        return function(event) {
-          return _this.renderPartialGraph();
-        };
-      })(this));
-    };
-
-    BranchGraph.prototype.scrollDown = function() {
-      this.element.scrollTop(this.element.scrollTop() + 50);
-      return this.renderPartialGraph();
-    };
-
-    BranchGraph.prototype.scrollUp = function() {
-      this.element.scrollTop(this.element.scrollTop() - 50);
-      return this.renderPartialGraph();
-    };
-
-    BranchGraph.prototype.scrollLeft = function() {
-      this.element.scrollLeft(this.element.scrollLeft() - 50);
-      return this.renderPartialGraph();
-    };
-
-    BranchGraph.prototype.scrollRight = function() {
-      this.element.scrollLeft(this.element.scrollLeft() + 50);
-      return this.renderPartialGraph();
-    };
-
-    BranchGraph.prototype.scrollBottom = function() {
-      return this.element.scrollTop(this.element.find('svg').height());
-    };
+      return this.top.toFront();
+    }
+  };
 
-    BranchGraph.prototype.scrollTop = function() {
-      return this.element.scrollTop(0);
-    };
+  BranchGraph.prototype.bindEvents = function() {
+    var element;
+    element = this.element;
+    return $(element).scroll((function(_this) {
+      return function(event) {
+        return _this.renderPartialGraph();
+      };
+    })(this));
+  };
 
-    BranchGraph.prototype.appendLabel = function(x, y, commit) {
-      var label, r, rect, shortrefs, text, textbox, triangle;
-      if (!commit.refs) {
-        return;
-      }
-      r = this.r;
-      shortrefs = commit.refs;
-      // Truncate if longer than 15 chars
-      if (shortrefs.length > 17) {
-        shortrefs = shortrefs.substr(0, 15) + "…";
-      }
-      text = r.text(x + 4, y, shortrefs).attr({
-        "text-anchor": "start",
-        font: "10px Monaco, monospace",
-        fill: "#FFF",
-        title: commit.refs
-      });
-      textbox = text.getBBox();
-      // Create rectangle based on the size of the textbox
-      rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
-        fill: "#000",
-        "fill-opacity": .5,
-        stroke: "none"
-      });
-      triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({
-        fill: "#000",
-        "fill-opacity": .5,
-        stroke: "none"
-      });
-      label = r.set(rect, text);
-      label.transform(["t", -rect.getBBox().width - 15, 0]);
-      // Set text to front
-      return text.toFront();
-    };
+  BranchGraph.prototype.scrollDown = function() {
+    this.element.scrollTop(this.element.scrollTop() + 50);
+    return this.renderPartialGraph();
+  };
 
-    BranchGraph.prototype.appendAnchor = function(x, y, commit) {
-      var anchor, options, r, top;
-      r = this.r;
-      top = this.top;
-      options = this.options;
-      anchor = r.circle(x, y, 10).attr({
-        fill: "#000",
-        opacity: 0,
-        cursor: "pointer"
-      }).click(function() {
-        return window.open(options.commit_url.replace("%s", commit.id), "_blank");
-      }).hover(function() {
-        this.tooltip = r.commitTooltip(x + 5, y, commit);
-        return top.push(this.tooltip.insertBefore(this));
-      }, function() {
-        return this.tooltip && this.tooltip.remove() && delete this.tooltip;
-      });
-      return top.push(anchor);
-    };
+  BranchGraph.prototype.scrollUp = function() {
+    this.element.scrollTop(this.element.scrollTop() - 50);
+    return this.renderPartialGraph();
+  };
 
-    BranchGraph.prototype.drawDot = function(x, y, commit) {
-      var avatar_box_x, avatar_box_y, r;
-      r = this.r;
-      r.circle(x, y, 3).attr({
-        fill: this.colors[commit.space],
-        stroke: "none"
-      });
-      avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
-      avatar_box_y = y - 10;
-      r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
-        stroke: this.colors[commit.space],
-        "stroke-width": 2
-      });
-      r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20);
-      return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({
-        "text-anchor": "start",
-        font: "14px Monaco, monospace"
-      });
-    };
+  BranchGraph.prototype.scrollLeft = function() {
+    this.element.scrollLeft(this.element.scrollLeft() - 50);
+    return this.renderPartialGraph();
+  };
 
-    BranchGraph.prototype.drawLines = function(x, y, commit) {
-      var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
-      r = this.r;
-      ref = commit.parents;
-      results = [];
-      for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
-        parent = ref[i];
-        parentCommit = this.preparedCommits[parent[0]];
-        parentY = this.offsetY + this.unitTime * parentCommit.time;
-        parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
-        parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
-        // Set line color
-        if (parentCommit.space <= commit.space) {
-          color = this.colors[commit.space];
-        } else {
-          color = this.colors[parentCommit.space];
-        }
-        // Build line shape
-        if (parent[1] === commit.space) {
-          offset = [0, 5];
-          arrow = "l-2,5,4,0,-2,-5,0,5";
-        } else if (parent[1] < commit.space) {
-          offset = [3, 3];
-          arrow = "l5,0,-2,4,-3,-4,4,2";
-        } else {
-          offset = [-3, 3];
-          arrow = "l-5,0,2,4,3,-4,-4,2";
-        }
-        // Start point
-        route = ["M", x + offset[0], y + offset[1]];
-        // Add arrow if not first parent
-        if (i > 0) {
-          route.push(arrow);
-        }
-        // Circumvent if overlap
-        if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
-          route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
-        }
-        // End point
-        route.push("L", parentX1, parentY);
-        results.push(r.path(route).attr({
-          stroke: color,
-          "stroke-width": 2
-        }));
-      }
-      return results;
-    };
+  BranchGraph.prototype.scrollRight = function() {
+    this.element.scrollLeft(this.element.scrollLeft() + 50);
+    return this.renderPartialGraph();
+  };
 
-    BranchGraph.prototype.markCommit = function(commit) {
-      var r, x, y;
-      if (commit.id === this.options.commit_id) {
-        r = this.r;
-        x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
-        y = this.offsetY + this.unitTime * commit.time;
-        r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
-          fill: "#000",
-          "fill-opacity": .5,
-          stroke: "none"
-        });
-        // Displayed in the center
-        return this.element.scrollTop(y - this.graphHeight / 2);
-      }
-    };
+  BranchGraph.prototype.scrollBottom = function() {
+    return this.element.scrollTop(this.element.find('svg').height());
+  };
 
-    return BranchGraph;
-  })();
+  BranchGraph.prototype.scrollTop = function() {
+    return this.element.scrollTop(0);
+  };
 
-  Raphael.prototype.commitTooltip = function(x, y, commit) {
-    var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip;
-    boxWidth = 300;
-    boxHeight = 200;
-    icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
-    nameText = this.text(x + 25, y + 10, commit.author.name);
-    idText = this.text(x, y + 35, commit.id);
-    messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
-    textSet = this.set(icon, nameText, idText, messageText).attr({
+  BranchGraph.prototype.appendLabel = function(x, y, commit) {
+    var label, r, rect, shortrefs, text, textbox, triangle;
+    if (!commit.refs) {
+      return;
+    }
+    r = this.r;
+    shortrefs = commit.refs;
+    // Truncate if longer than 15 chars
+    if (shortrefs.length > 17) {
+      shortrefs = shortrefs.substr(0, 15) + "…";
+    }
+    text = r.text(x + 4, y, shortrefs).attr({
       "text-anchor": "start",
-      font: "12px Monaco, monospace"
-    });
-    nameText.attr({
-      font: "14px Arial",
-      "font-weight": "bold"
+      font: "10px Monaco, monospace",
+      fill: "#FFF",
+      title: commit.refs
     });
-    idText.attr({
-      fill: "#AAA"
+    textbox = text.getBBox();
+    // Create rectangle based on the size of the textbox
+    rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
+      fill: "#000",
+      "fill-opacity": .5,
+      stroke: "none"
     });
-    messageText.node.style["white-space"] = "pre";
-    this.textWrap(messageText, boxWidth - 50);
-    rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
-      fill: "#FFF",
-      stroke: "#000",
-      "stroke-linecap": "round",
-      "stroke-width": 2
+    triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({
+      fill: "#000",
+      "fill-opacity": .5,
+      stroke: "none"
     });
-    tooltip = this.set(rect, textSet);
-    rect.attr({
-      height: tooltip.getBBox().height + 10,
-      width: tooltip.getBBox().width + 10
+    label = r.set(rect, text);
+    label.transform(["t", -rect.getBBox().width - 15, 0]);
+    // Set text to front
+    return text.toFront();
+  };
+
+  BranchGraph.prototype.appendAnchor = function(x, y, commit) {
+    var anchor, options, r, top;
+    r = this.r;
+    top = this.top;
+    options = this.options;
+    anchor = r.circle(x, y, 10).attr({
+      fill: "#000",
+      opacity: 0,
+      cursor: "pointer"
+    }).click(function() {
+      return window.open(options.commit_url.replace("%s", commit.id), "_blank");
+    }).hover(function() {
+      this.tooltip = r.commitTooltip(x + 5, y, commit);
+      return top.push(this.tooltip.insertBefore(this));
+    }, function() {
+      return this.tooltip && this.tooltip.remove() && delete this.tooltip;
     });
-    tooltip.transform(["t", 20, 20]);
-    return tooltip;
+    return top.push(anchor);
   };
 
-  Raphael.prototype.textWrap = function(t, width) {
-    var abc, b, content, h, j, len, letterWidth, s, word, words, x;
-    content = t.attr("text");
-    abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
-    t.attr({
-      text: abc
+  BranchGraph.prototype.drawDot = function(x, y, commit) {
+    var avatar_box_x, avatar_box_y, r;
+    r = this.r;
+    r.circle(x, y, 3).attr({
+      fill: this.colors[commit.space],
+      stroke: "none"
     });
-    letterWidth = t.getBBox().width / abc.length;
-    t.attr({
-      text: content
+    avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+    avatar_box_y = y - 10;
+    r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
+      stroke: this.colors[commit.space],
+      "stroke-width": 2
     });
-    words = content.split(" ");
-    x = 0;
-    s = [];
-    for (j = 0, len = words.length; j < len; j += 1) {
-      word = words[j];
-      if (x + (word.length * letterWidth) > width) {
-        s.push("\n");
-        x = 0;
+    r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20);
+    return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({
+      "text-anchor": "start",
+      font: "14px Monaco, monospace"
+    });
+  };
+
+  BranchGraph.prototype.drawLines = function(x, y, commit) {
+    var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
+    r = this.r;
+    ref = commit.parents;
+    results = [];
+    for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+      parent = ref[i];
+      parentCommit = this.preparedCommits[parent[0]];
+      parentY = this.offsetY + this.unitTime * parentCommit.time;
+      parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
+      parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+      // Set line color
+      if (parentCommit.space <= commit.space) {
+        color = this.colors[commit.space];
+      } else {
+        color = this.colors[parentCommit.space];
       }
-      if (word === "\n") {
-        s.push("\n");
-        x = 0;
+      // Build line shape
+      if (parent[1] === commit.space) {
+        offset = [0, 5];
+        arrow = "l-2,5,4,0,-2,-5,0,5";
+      } else if (parent[1] < commit.space) {
+        offset = [3, 3];
+        arrow = "l5,0,-2,4,-3,-4,4,2";
       } else {
-        s.push(word + " ");
-        x += word.length * letterWidth;
+        offset = [-3, 3];
+        arrow = "l-5,0,2,4,3,-4,-4,2";
+      }
+      // Start point
+      route = ["M", x + offset[0], y + offset[1]];
+      // Add arrow if not first parent
+      if (i > 0) {
+        route.push(arrow);
+      }
+      // Circumvent if overlap
+      if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
+        route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
       }
+      // End point
+      route.push("L", parentX1, parentY);
+      results.push(r.path(route).attr({
+        stroke: color,
+        "stroke-width": 2
+      }));
+    }
+    return results;
+  };
+
+  BranchGraph.prototype.markCommit = function(commit) {
+    var r, x, y;
+    if (commit.id === this.options.commit_id) {
+      r = this.r;
+      x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+      y = this.offsetY + this.unitTime * commit.time;
+      r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
+        fill: "#000",
+        "fill-opacity": .5,
+        stroke: "none"
+      });
+      // Displayed in the center
+      return this.element.scrollTop(y - this.graphHeight / 2);
     }
-    t.attr({
-      text: s.join("").trim()
-    });
-    b = t.getBBox();
-    h = Math.abs(b.y2) + 1;
-    return t.attr({
-      y: h
-    });
   };
-}).call(window);
+
+  return BranchGraph;
+})();
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 8e7027b44e7c996e9bfe3308e6a5f672c1b956f5..a3fd22aff2a97065e0ff15d70dbfd5ff1a672b0c 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,20 +1,19 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
-/* global BranchGraph */
 
-(function() {
-  this.Network = (function() {
-    function Network(opts) {
-      var vph;
-      $("#filter_ref").click(function() {
-        return $(this).closest('form').submit();
-      });
-      this.branch_graph = new BranchGraph($(".network-graph"), opts);
-      vph = $(window).height() - 250;
-      $('.network-graph').css({
-        'height': vph + 'px'
-      });
-    }
+import BranchGraph from './branch_graph';
 
-    return Network;
-  })();
-}).call(window);
+export default (function() {
+  function Network(opts) {
+    var vph;
+    $("#filter_ref").click(function() {
+      return $(this).closest('form').submit();
+    });
+    this.branch_graph = new BranchGraph($(".network-graph"), opts);
+    vph = $(window).height() - 250;
+    $('.network-graph').css({
+      'height': vph + 'px'
+    });
+  }
+
+  return Network;
+})();
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index aae509caa799e4fd7f1f3b005a16ad9f76117124..8aae2ad201cccd3e5c17e4bedc80e788f4b7ee37 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,22 +1,17 @@
 /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
-/* global Network */
 /* global ShortcutsNetwork */
 
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
+import Network from './network';
 
-(function() {
-  $(function() {
-    if (!$(".network-graph").length) return;
+$(function() {
+  if (!$(".network-graph").length) return;
 
-    var network_graph;
-    network_graph = new Network({
-      url: $(".network-graph").attr('data-url'),
-      commit_url: $(".network-graph").attr('data-commit-url'),
-      ref: $(".network-graph").attr('data-ref'),
-      commit_id: $(".network-graph").attr('data-commit-id')
-    });
-    return new ShortcutsNetwork(network_graph.branch_graph);
+  var network_graph;
+  network_graph = new Network({
+    url: $(".network-graph").attr('data-url'),
+    commit_url: $(".network-graph").attr('data-commit-url'),
+    ref: $(".network-graph").attr('data-ref'),
+    commit_id: $(".network-graph").attr('data-commit-id')
   });
-}).call(window);
+  return new ShortcutsNetwork(network_graph.branch_graph);
+});
diff --git a/app/assets/javascripts/network/raphael.js b/app/assets/javascripts/network/raphael.js
new file mode 100644
index 0000000000000000000000000000000000000000..09dcf716148d6a939f61b24bcaa39cb7b2ce3e84
--- /dev/null
+++ b/app/assets/javascripts/network/raphael.js
@@ -0,0 +1,74 @@
+import Raphael from 'raphael/raphael';
+
+Raphael.prototype.commitTooltip = function commitTooltip(x, y, commit) {
+  const boxWidth = 300;
+  const icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
+  const nameText = this.text(x + 25, y + 10, commit.author.name);
+  const idText = this.text(x, y + 35, commit.id);
+  const messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, ' \n '));
+  const textSet = this.set(icon, nameText, idText, messageText).attr({
+    'text-anchor': 'start',
+    font: '12px Monaco, monospace',
+  });
+  nameText.attr({
+    font: '14px Arial',
+    'font-weight': 'bold',
+  });
+  idText.attr({
+    fill: '#AAA',
+  });
+  messageText.node.style['white-space'] = 'pre';
+  this.textWrap(messageText, boxWidth - 50);
+  const rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
+    fill: '#FFF',
+    stroke: '#000',
+    'stroke-linecap': 'round',
+    'stroke-width': 2,
+  });
+  const tooltip = this.set(rect, textSet);
+  rect.attr({
+    height: tooltip.getBBox().height + 10,
+    width: tooltip.getBBox().width + 10,
+  });
+  tooltip.transform(['t', 20, 20]);
+  return tooltip;
+};
+
+Raphael.prototype.textWrap = function testWrap(t, width) {
+  const content = t.attr('text');
+  const abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+  t.attr({
+    text: abc,
+  });
+  const letterWidth = t.getBBox().width / abc.length;
+  t.attr({
+    text: content,
+  });
+  const words = content.split(' ');
+  let x = 0;
+  const s = [];
+  for (let j = 0, len = words.length; j < len; j += 1) {
+    const word = words[j];
+    if (x + (word.length * letterWidth) > width) {
+      s.push('\n');
+      x = 0;
+    }
+    if (word === '\n') {
+      s.push('\n');
+      x = 0;
+    } else {
+      s.push(`${word} `);
+      x += word.length * letterWidth;
+    }
+  }
+  t.attr({
+    text: s.join('').trim(),
+  });
+  const b = t.getBBox();
+  const h = Math.abs(b.y2) + 1;
+  return t.attr({
+    y: h,
+  });
+};
+
+export default Raphael;
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index cb24f212c66e818328bcbad05397f3b8943af9e5..5828f460a235a1f9a25a9019fdf26cb0361ee5b5 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
 (function() {
   var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
     indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -20,15 +20,35 @@
     };
 
     NewBranchForm.prototype.init = function() {
-      if (this.name.val().length > 0) {
+      if (this.name.length && this.name.val().length > 0) {
         return this.name.trigger('blur');
       }
     };
 
     NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
-      return this.ref.autocomplete({
-        source: availableRefs,
-        minLength: 1
+      var $branchSelect = $('.js-branch-select');
+
+      $branchSelect.glDropdown({
+        data: availableRefs,
+        filterable: true,
+        filterByText: true,
+        remote: false,
+        fieldName: $branchSelect.data('field-name'),
+        selectable: true,
+        isSelectable: function(branch, $el) {
+          return !$el.hasClass('is-active');
+        },
+        text: function(branch) {
+          return branch;
+        },
+        id: function(branch) {
+          return branch;
+        },
+        toggleLabel: function(branch) {
+          if (branch) {
+            return branch;
+          }
+        }
       });
     };
 
@@ -61,7 +81,7 @@
       var errorMessage, errors, formatter, unique, validator;
       this.branchNameError.empty();
       unique = function(values, value) {
-        if (indexOf.call(values, value) < 0) {
+        if (indexOf.call(values, value) === -1) {
           values.push(value);
         }
         return values;
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 747f693726e131a252c3786da65b4571bbe68c12..ad36f08840d8c4012eaf0ac491073863af3dd63d 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -3,19 +3,23 @@
   var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
 
   this.NewCommitForm = (function() {
-    function NewCommitForm(form) {
+    function NewCommitForm(form, targetBranchName = 'target_branch') {
+      this.form = form;
+      this.targetBranchName = targetBranchName;
       this.renderDestination = bind(this.renderDestination, this);
-      this.newBranch = form.find('.js-target-branch');
+      this.targetBranchDropdown = form.find('button.js-target-branch');
       this.originalBranch = form.find('.js-original-branch');
       this.createMergeRequest = form.find('.js-create-merge-request');
       this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+      this.targetBranchDropdown.on('change.branch', this.renderDestination);
       this.renderDestination();
-      this.newBranch.keyup(this.renderDestination);
     }
 
     NewCommitForm.prototype.renderDestination = function() {
       var different;
-      different = this.newBranch.val() !== this.originalBranch.val();
+      var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
+
+      different = targetBranch.val() !== this.originalBranch.val();
       if (different) {
         this.createMergeRequestContainer.show();
         if (!this.wasDifferent) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 03504255bdae3632cf0fee62e41a23d41d150947..47cc34e7a20fd816a89b043f54a94013263bbef5 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,12 +1,14 @@
 /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, 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, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
 /* global Flash */
 /* global Autosave */
+/* global Cookies */
 /* global ResolveService */
 /* global mrRefreshWidgetUrl */
 
 require('./autosave');
 window.autosize = require('vendor/autosize');
 window.Dropzone = require('dropzone');
+window.Cookies = require('js-cookie');
 require('./dropzone_input');
 require('./gfm_auto_complete');
 require('vendor/jquery.caret'); // required by jquery.atwho
@@ -42,7 +44,6 @@ require('./task_list');
       this.notes_url = notes_url;
       this.note_ids = note_ids;
       this.last_fetched_at = last_fetched_at;
-      this.view = view;
       this.noteable_url = document.URL;
       this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
       this.basePollingInterval = 15000;
@@ -57,6 +58,7 @@ require('./task_list');
         selector: '.notes'
       });
       this.collapseLongCommitList();
+      this.setViewType(view);
 
       // We are in the Merge Requests page so we need another edit form for Changes tab
       if (gl.utils.getPagePath(1) === 'merge_requests') {
@@ -65,6 +67,10 @@ require('./task_list');
       }
     }
 
+    Notes.prototype.setViewType = function(view) {
+      this.view = Cookies.get('diff_view') || view;
+    };
+
     Notes.prototype.addBinding = function() {
       // add note to UI after creation
       $(document).on("ajax:success", ".js-main-target-form", this.addNote);
@@ -198,7 +204,7 @@ require('./task_list');
       this.refreshing = true;
       return $.ajax({
         url: this.notes_url,
-        data: "last_fetched_at=" + this.last_fetched_at,
+        headers: { "X-Last-Fetched-At": this.last_fetched_at },
         dataType: "json",
         success: (function(_this) {
           return function(data) {
@@ -246,12 +252,21 @@ require('./task_list');
     };
 
     Notes.prototype.handleCreateChanges = function(note) {
+      var votesBlock;
       if (typeof note === 'undefined') {
         return;
       }
 
-      if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
-        $.get(mrRefreshWidgetUrl);
+      if (note.commands_changes) {
+        if ('merge' in note.commands_changes) {
+          $.get(mrRefreshWidgetUrl);
+        }
+
+        if ('emoji_award' in note.commands_changes) {
+          votesBlock = $('.js-awards-block').eq(0);
+          gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
+          return gl.awardsHandler.scrollToAwards();
+        }
       }
     };
 
@@ -262,26 +277,16 @@ require('./task_list');
      */
 
     Notes.prototype.renderNote = function(note) {
-      var $notesList, votesBlock;
+      var $notesList;
       if (!note.valid) {
-        if (note.award) {
-          new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
-        }
-        else {
-          if (note.errors.commands_only) {
-            new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
-            this.refresh();
-          }
+        if (note.errors.commands_only) {
+          new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+          this.refresh();
         }
         return;
       }
-      if (note.award) {
-        votesBlock = $('.js-awards-block').eq(0);
-        gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
-        return gl.awardsHandler.scrollToAwards();
-      // render note if it not present in loaded list
-      // or skip if rendered
-      } else if (this.isNewNote(note)) {
+
+      if (this.isNewNote(note)) {
         this.note_ids.push(note.id);
         $notesList = $('ul.main-notes-list');
         $notesList.append(note.html).syntaxHighlight();
@@ -303,7 +308,7 @@ require('./task_list');
     };
 
     Notes.prototype.isParallelView = function() {
-      return this.view === 'parallel';
+      return Cookies.get('diff_view') === 'parallel';
     };
 
     /*
@@ -313,7 +318,7 @@ require('./task_list');
      */
 
     Notes.prototype.renderDiscussionNote = function(note) {
-      var discussionContainer, form, note_html, row;
+      var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
       if (!this.isNewNote(note)) {
         return;
       }
@@ -323,6 +328,8 @@ require('./task_list');
         form = $("#new-discussion-note-form-" + note.original_discussion_id);
       }
       row = form.closest("tr");
+      lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
+      diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
       note_html = $(note.html);
       note_html.renderGFM();
       // is this the first note of discussion?
@@ -331,10 +338,26 @@ require('./task_list');
         discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
       }
       if (discussionContainer.length === 0) {
-        // insert the note and the reply button after the temp row
-        row.after(note.diff_discussion_html);
-        // remove the note (will be added again below)
-        row.next().find(".note").remove();
+        if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
+          // insert the note and the reply button after the temp row
+          row.after(note.diff_discussion_html);
+
+          // remove the note (will be added again below)
+          row.next().find(".note").remove();
+        } else {
+          // Merge new discussion HTML in
+          var $discussion = $(note.diff_discussion_html);
+          var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
+          var contentContainerClass = '.' + $notes.closest('.notes_content')
+            .attr('class')
+            .split(' ')
+            .join('.');
+
+          // remove the note (will be added again below)
+          $notes.find('.note').remove();
+
+          row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
+        }
         // Before that, the container didn't exist
         discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
         // Add note to 'Changes' page discussions
@@ -348,14 +371,40 @@ require('./task_list');
         discussionContainer.append(note_html);
       }
 
-      if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+      if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
         gl.diffNotesCompileComponents();
+        this.renderDiscussionAvatar(diffAvatarContainer, note);
       }
 
       gl.utils.localTimeAgo($('.js-timeago'), false);
       return this.updateNotesCount(1);
     };
 
+    Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
+      return $(changesDiscussionContainer).closest('.notes_holder')
+        .prevAll('.line_holder')
+        .first()
+        .get(0);
+    };
+
+    Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
+      var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
+      var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
+
+      if (!avatarHolder.length) {
+        avatarHolder = document.createElement('diff-note-avatars');
+        avatarHolder.setAttribute('discussion-id', note.discussion_id);
+
+        diffAvatarContainer.append(avatarHolder);
+
+        gl.diffNotesCompileComponents();
+      }
+
+      if (commentButton.length) {
+        commentButton.remove();
+      }
+    };
+
     /*
     Called in response the main target form has been successfully submitted.
 
@@ -593,9 +642,14 @@ require('./task_list');
      */
 
     Notes.prototype.removeNote = function(e) {
-      var noteId;
-      noteId = $(e.currentTarget).closest(".note").attr("id");
-      $(".note[id='" + noteId + "']").each((function(_this) {
+      var noteElId, noteId, dataNoteId, $note, lineHolder;
+      $note = $(e.currentTarget).closest('.note');
+      noteElId = $note.attr('id');
+      noteId = $note.attr('data-note-id');
+      lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
+        .closest('.notes_holder')
+        .prev('.line_holder');
+      $(".note[id='" + noteElId + "']").each((function(_this) {
         // A same note appears in the "Discussion" and in the "Changes" tab, we have
         // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
         // where $("#noteId") would return only one.
@@ -605,17 +659,26 @@ require('./task_list');
           notes = note.closest(".notes");
 
           if (typeof gl.diffNotesCompileComponents !== 'undefined') {
-            if (gl.diffNoteApps[noteId]) {
-              gl.diffNoteApps[noteId].$destroy();
+            if (gl.diffNoteApps[noteElId]) {
+              gl.diffNoteApps[noteElId].$destroy();
             }
           }
 
+          note.remove();
+
           // check if this is the last note for this line
-          if (notes.find(".note").length === 1) {
+          if (notes.find(".note").length === 0) {
+            var notesTr = notes.closest("tr");
+
             // "Discussions" tab
             notes.closest(".timeline-entry").remove();
-            // "Changes" tab / commit view
-            notes.closest("tr").remove();
+
+            if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
+              // "Changes" tab / commit view
+              notesTr.remove();
+            } else {
+              notes.closest('.content').empty();
+            }
           }
           return note.remove();
         };
@@ -708,15 +771,16 @@ require('./task_list');
      */
 
     Notes.prototype.addDiffNote = function(e) {
-      var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
+      var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
       e.preventDefault();
-      $link = $(e.currentTarget);
+      $link = $(e.currentTarget || e.target);
       row = $link.closest("tr");
       nextRow = row.next();
       hasNotes = nextRow.is(".notes_holder");
       addForm = false;
       notesContentSelector = ".notes_content";
       rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+      isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
       // In parallel view, look inside the correct left/right pane
       if (this.isParallelView()) {
         lineType = $link.data("lineType");
@@ -724,7 +788,9 @@ require('./task_list');
         rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
       }
       notesContentSelector += " .content";
-      if (hasNotes) {
+      notesContent = nextRow.find(notesContentSelector);
+
+      if (hasNotes && !isDiffCommentAvatar) {
         nextRow.show();
         notesContent = nextRow.find(notesContentSelector);
         if (notesContent.length) {
@@ -741,13 +807,21 @@ require('./task_list');
             }
           }
         }
-      } else {
+      } else if (!isDiffCommentAvatar) {
         // add a notes row and insert the form
         row.after(rowCssToAdd);
         nextRow = row.next();
         notesContent = nextRow.find(notesContentSelector);
         addForm = true;
+      } else {
+        nextRow.show();
+        notesContent.toggle(!notesContent.is(':visible'));
+
+        if (!nextRow.find('.content:not(:empty)').is(':visible')) {
+          nextRow.hide();
+        }
       }
+
       if (addForm) {
         newForm = this.formClone.clone();
         newForm.appendTo(notesContent);
diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js
similarity index 86%
rename from app/assets/javascripts/pager.js.es6
rename to app/assets/javascripts/pager.js
index e35cf6d295e37c4a22f4464cf8927b35dcb507fb..5f6bc902cf856acef181ae80165ba91789368c09 100644
--- a/app/assets/javascripts/pager.js.es6
+++ b/app/assets/javascripts/pager.js
@@ -1,11 +1,15 @@
+require('~/lib/utils/common_utils');
+require('~/lib/utils/url_utility');
+
 (() => {
   const ENDLESS_SCROLL_BOTTOM_PX = 400;
   const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
 
   const Pager = {
     init(limit = 0, preload = false, disable = false, callback = $.noop) {
+      this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
       this.limit = limit;
-      this.offset = this.limit;
+      this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
       this.disable = disable;
       this.callback = callback;
       this.loading = $('.loading').first();
@@ -20,7 +24,7 @@
       this.loading.show();
       $.ajax({
         type: 'GET',
-        url: $('.content_list').data('href') || window.location.href,
+        url: this.url,
         data: `limit=${this.limit}&offset=${this.offset}`,
         dataType: 'json',
         error: () => this.loading.hide(),
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js
similarity index 100%
rename from app/assets/javascripts/pipelines.js.es6
rename to app/assets/javascripts/pipelines.js
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js
similarity index 98%
rename from app/assets/javascripts/profile/gl_crop.js.es6
rename to app/assets/javascripts/profile/gl_crop.js
index 42e9847af919c1101437e7fff744af722494c033..cf1566eeb87b3ebc2c1ab4f548b4c9bdc8429c8e 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -1,5 +1,7 @@
 /* eslint-disable no-useless-escape, max-len, 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, new-parens */
 
+import 'vendor/cropper';
+
 ((global) => {
   // Matches everything but the file name
   const FILENAMEREGEX = /^.*[\\\/]/;
@@ -13,7 +15,7 @@
       this.onPickImageClick = this.onPickImageClick.bind(this);
       this.fileInput = $(input);
       this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
-      this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
+      this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`);
       this.exportWidth = exportWidth;
       this.exportHeight = exportHeight;
       this.cropBoxWidth = cropBoxWidth;
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js
similarity index 92%
rename from app/assets/javascripts/profile/profile.js.es6
rename to app/assets/javascripts/profile/profile.js
index 81374296522ee8f18014bbb4173ac00f7b0a5c69..c38bc762675f69ea0a0a8fa00bfe2a0c006a7ed1 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js
@@ -25,7 +25,6 @@
     bindEvents() {
       $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
       $('#user_notification_email').on('change', this.submitForm);
-      $('#user_notified_of_own_activity').on('change', this.submitForm);
       $('.update-username').on('ajax:before', this.beforeUpdateUsername);
       $('.update-username').on('ajax:complete', this.afterUpdateUsername);
       $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
@@ -84,13 +83,14 @@
   }
 
   $(function() {
-    $(document).on('focusout.ssh_key', '#key_key', function() {
+    $(document).on('input.ssh_key', '#key_key', function() {
       const $title = $('#key_title');
       const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
-      if (comment && comment.length > 1 && $title.val() === '') {
+
+      // Extract the SSH Key title from its comment
+      if (comment && comment.length > 1) {
         return $title.val(comment[1]).change();
       }
-    // Extract the SSH Key title from its comment
     });
     if (global.utils.getPagePath() === 'profiles') {
       return new Profile();
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index d7f3c9fd37e167c4937a6bc1d31e5bb6ae35e574..15d32825583cb984586f0b47d1273055ec5789bf 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,3 +1,2 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
+require('./gl_crop');
+require('./profile');
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 7c03c8b72d4b15a11ee6e2642b9af2b711712706..db7ceaa2421d826e70142e58bb5bb6c3edc156f0 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -116,7 +116,7 @@
             if ($('input[name="ref"]').length) {
               var $form = $dropdown.closest('form');
               var action = $form.attr('action');
-              var divider = action.indexOf('?') < 0 ? '?' : '&';
+              var divider = action.indexOf('?') === -1 ? '?' : '&';
               gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
             }
           }
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js
similarity index 100%
rename from app/assets/javascripts/project_label_subscription.js.es6
rename to app/assets/javascripts/project_label_subscription.js
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index f80e765ce302369fd6bd059a3ed94d6ec3829d8c..3c1c1e7dcebbf18616edf52fee7d574550f12941 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -35,7 +35,7 @@
             if (this.groupId) {
               return Api.groupProjects(this.groupId, term, projectsCallback);
             } else {
-              return Api.projects(term, orderBy, projectsCallback);
+              return Api.projects(term, { order_by: orderBy }, projectsCallback);
             }
           },
           url: function(project) {
@@ -84,7 +84,7 @@
               if (_this.groupId) {
                 return Api.groupProjects(_this.groupId, query.term, projectsCallback);
               } else {
-                return Api.projects(query.term, _this.orderBy, projectsCallback);
+                return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback);
               }
             };
           })(this),
diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js
similarity index 100%
rename from app/assets/javascripts/project_variables.js.es6
rename to app/assets/javascripts/project_variables.js
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index acdf9b7eb5a5b9f201a7eae6e3a0878d30c61752..c67d59d2be58ba2670ea026231ada53f1166b08b 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,50 +1,18 @@
-/* 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, max-len */
+import FilterableList from './filterable_list';
 
-(function() {
-  window.ProjectsList = {
-    init: function() {
-      $(".projects-list-filter").off('keyup');
-      this.initSearch();
-      return this.initPagination();
-    },
-    initSearch: function() {
-      var debounceFilter, projectsListFilter;
-      projectsListFilter = $('.projects-list-filter');
-      debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
-      return projectsListFilter.on('keyup', function(e) {
-        if (projectsListFilter.val() !== '') {
-          return debounceFilter();
-        }
-      });
-    },
-    filterResults: function() {
-      var form, project_filter_url, search;
-      $('.projects-list-holder').fadeTo(250, 0.5);
-      form = null;
-      form = $("form#project-filter-form");
-      search = $(".projects-list-filter").val();
-      project_filter_url = form.attr('action') + '?' + form.serialize();
-      return $.ajax({
-        type: "GET",
-        url: form.attr('action'),
-        data: form.serialize(),
-        complete: function() {
-          return $('.projects-list-holder').fadeTo(250, 1);
-        },
-        success: function(data) {
-          $('.projects-list-holder').replaceWith(data.html);
-          return history.replaceState({
-            page: project_filter_url
-          // Change url so if user reload a page - search results are saved
-          }, document.title, project_filter_url);
-        },
-        dataType: "json"
-      });
-    },
-    initPagination: function() {
-      return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) {
-        return $('.projects-list-holder').replaceWith(data.html);
-      });
+/**
+ * Makes search request for projects when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+export default class ProjectsList {
+  constructor() {
+    const form = document.querySelector('form#project-filter-form');
+    const filter = document.querySelector('.js-projects-list-filter');
+    const holder = document.querySelector('.js-projects-list-holder');
+
+    if (form && filter && holder) {
+      const list = new FilterableList(form, filter, holder);
+      list.initSearch();
     }
-  };
-}).call(window);
+  }
+}
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
similarity index 100%
rename from app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js
similarity index 100%
rename from app/assets/javascripts/protected_branches/protected_branch_create.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_create.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
similarity index 100%
rename from app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_dropdown.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js
similarity index 90%
rename from app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit.js
index 149e511451e944ddf5ff15d91dffa23b3ace29f8..6ef59e94384f976ac55fe536721645ce3b375e83 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -36,6 +36,9 @@
       // Do not update if one dropdown has not selected any option
       if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
 
+      this.$allowedToMergeDropdown.disable();
+      this.$allowedToPushDropdown.disable();
+
       $.ajax({
         type: 'POST',
         url: this.$wrap.data('url'),
@@ -53,13 +56,13 @@
             }]
           }
         },
-        success: () => {
-          this.$wrap.effect('highlight');
-        },
         error() {
           $.scrollTo(0);
           new Flash('Failed to update branch!');
         }
+      }).always(() => {
+        this.$allowedToMergeDropdown.enable();
+        this.$allowedToPushDropdown.enable();
       });
     }
   };
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
similarity index 100%
rename from app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit_list.js
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index ffb66caf5f45dae37a5d1e9c5ddd5cf36187a8d2..849c1e31623bc89cb84914504edce3c9e4bf37dd 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,3 +1,5 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
+require('./protected_branch_access_dropdown');
+require('./protected_branch_create');
+require('./protected_branch_dropdown');
+require('./protected_branch_edit');
+require('./protected_branch_edit_list');
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index e66418beeabf78d9c788e5997f6bd0876638e524..15f5963353a1f159b039f9864b381fcadd93aa91 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -47,7 +47,7 @@
           fields: ['name']
         },
         data: function(term, callback) {
-          return Api.projects(term, 'id', function(data) {
+          return Api.projects(term, { order_by: 'id' }, function(data) {
             data.unshift({
               name_with_namespace: 'Any'
             });
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js
similarity index 100%
rename from app/assets/javascripts/search_autocomplete.js.es6
rename to app/assets/javascripts/search_autocomplete.js
diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js
similarity index 100%
rename from app/assets/javascripts/shortcuts_blob.js.es6
rename to app/assets/javascripts/shortcuts_blob.js
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 542cd586df0ed7971f773c944d830c0364dd88af..09a58cad2b2a9fa09465f11845173bd10672b7ee 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -32,7 +32,7 @@ require('./shortcuts');
         return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
       });
       Mousetrap.bind('g g', function() {
-        return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs');
+        return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts');
       });
       Mousetrap.bind('g i', function() {
         return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js
similarity index 100%
rename from app/assets/javascripts/signin_tabs_memoizer.js.es6
rename to app/assets/javascripts/signin_tabs_memoizer.js
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js
similarity index 100%
rename from app/assets/javascripts/smart_interval.js.es6
rename to app/assets/javascripts/smart_interval.js
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 89822246bb82b1073bd45f1efe4acda420febba1..a98403f4cf2a4ff8aeb258ad26ff47a308105078 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,10 +1,6 @@
 /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
 /* global ace */
 
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
-
 (function() {
   $(function() {
     var editor = ace.edit("editor");
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js
similarity index 100%
rename from app/assets/javascripts/snippets_list.js.es6
rename to app/assets/javascripts/snippets_list.js
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js
similarity index 100%
rename from app/assets/javascripts/subbable_resource.js.es6
rename to app/assets/javascripts/subbable_resource.js
diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js
similarity index 100%
rename from app/assets/javascripts/subscription.js.es6
rename to app/assets/javascripts/subscription.js
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index dfe24d1fb336eb730fa5b2e419d59de05c245691..b1402c0a880c86ff5dbdf5ed6d8069c63e77d367 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,3 +1,4 @@
+/* global Flash */
 require('vendor/task_list');
 
 class TaskList {
@@ -6,6 +7,16 @@ class TaskList {
     this.dataType = options.dataType;
     this.fieldName = options.fieldName;
     this.onSuccess = options.onSuccess || (() => {});
+    this.onError = function showFlash(response) {
+      let errorMessages = '';
+
+      if (response.responseJSON) {
+        errorMessages = response.responseJSON.errors.join(' ');
+      }
+
+      return new Flash(errorMessages || 'Update failed', 'alert');
+    };
+
     this.init();
   }
 
@@ -32,6 +43,7 @@ class TaskList {
       url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
       data: patchData,
       success: this.onSuccess,
+      error: this.onError,
     });
   }
 }
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js
similarity index 100%
rename from app/assets/javascripts/templates/issuable_template_selector.js.es6
rename to app/assets/javascripts/templates/issuable_template_selector.js
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js
similarity index 100%
rename from app/assets/javascripts/templates/issuable_template_selectors.js.es6
rename to app/assets/javascripts/templates/issuable_template_selectors.js
diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js
similarity index 100%
rename from app/assets/javascripts/terminal/terminal.js.es6
rename to app/assets/javascripts/terminal/terminal.js
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js
similarity index 100%
rename from app/assets/javascripts/terminal/terminal_bundle.js.es6
rename to app/assets/javascripts/terminal/terminal_bundle.js
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index 7dba5840c8a8df52ae63f75e424661c1baf19ead..d48f2404fa587fdf08c32a5790d7d94e3c2641e8 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -43,7 +43,14 @@
     return event;
   }
 
-  function getTraget(target) {
+  function isLast(target) {
+    var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+    var children = el.children;
+
+    return children.length - 1 === target.index;
+  }
+
+  function getTarget(target) {
     var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
     var children = el.children;
 
@@ -75,12 +82,22 @@
   function simulateDrag(options, callback) {
     options.to.el = options.to.el || options.from.el;
 
-    var fromEl = getTraget(options.from);
-    var toEl = getTraget(options.to);
+    var fromEl = getTarget(options.from);
+    var toEl = getTarget(options.to);
+    var firstEl = getTarget({
+      el: options.to.el,
+      index: 'first'
+    });
+    var lastEl = getTarget({
+      el: options.to.el,
+      index: 'last'
+    });
     var scrollable = options.scrollable;
 
     var fromRect = getRect(fromEl);
     var toRect = getRect(toEl);
+    var firstRect = getRect(firstEl);
+    var lastRect = getRect(lastEl);
 
     var startTime = new Date().getTime();
     var duration = options.duration || 1000;
@@ -88,6 +105,12 @@
     options.ontap && options.ontap();
     window.SIMULATE_DRAG_ACTIVE = 1;
 
+    if (options.to.index === 0) {
+      toRect.cy = firstRect.y;
+    } else if (isLast(options.to)) {
+      toRect.cy = lastRect.y + lastRect.h + 50;
+    }
+
     var dragInterval = setInterval(function loop() {
       var progress = (new Date().getTime() - startTime) / duration;
       var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
new file mode 100644
index 0000000000000000000000000000000000000000..8be58023c8493d0259054889ca6a3bb93a0c7931
--- /dev/null
+++ b/app/assets/javascripts/todos.js
@@ -0,0 +1,163 @@
+/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
+/* global UsersSelect */
+
+class Todos {
+  constructor() {
+    this.initFilters();
+    this.bindEvents();
+    this.todo_ids = [];
+
+    this.cleanupWrapper = this.cleanup.bind(this);
+    document.addEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  cleanup() {
+    this.unbindEvents();
+    document.removeEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  unbindEvents() {
+    $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
+    $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
+    $('.todo').off('click', this.goToTodoUrl);
+  }
+
+  bindEvents() {
+    this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
+    this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this);
+
+    $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
+    $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
+    $('.todo').on('click', this.goToTodoUrl);
+  }
+
+  initFilters() {
+    this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
+    this.initFilterDropdown($('.js-type-search'), 'type');
+    this.initFilterDropdown($('.js-action-search'), 'action_id');
+
+    $('form.filter-form').on('submit', function applyFilters(event) {
+      event.preventDefault();
+      gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
+    });
+    return new UsersSelect();
+  }
+
+  initFilterDropdown($dropdown, fieldName, searchFields) {
+    $dropdown.glDropdown({
+      fieldName,
+      selectable: true,
+      filterable: searchFields ? true : false,
+      search: { fields: searchFields },
+      data: $dropdown.data('data'),
+      clicked: () => $dropdown.closest('form.filter-form').submit(),
+    });
+  }
+
+  updateRowStateClicked(e) {
+    e.preventDefault();
+
+    const target = e.target;
+    target.setAttribute('disabled', true);
+    target.classList.add('disabled');
+    $.ajax({
+      type: 'POST',
+      url: target.dataset.href,
+      dataType: 'json',
+      data: {
+        '_method': target.dataset.method,
+      },
+      success: (data) => {
+        this.updateRowState(target);
+        return this.updateBadges(data);
+      },
+    });
+  }
+
+  updateRowState(target) {
+    const row = target.closest('li');
+    const restoreBtn = row.querySelector('.js-undo-todo');
+    const doneBtn = row.querySelector('.js-done-todo');
+
+    target.classList.add('hidden');
+    target.removeAttribute('disabled');
+    target.classList.remove('disabled');
+
+    if (target === doneBtn) {
+      row.classList.add('done-reversible');
+      restoreBtn.classList.remove('hidden');
+    } else if (target === restoreBtn) {
+      row.classList.remove('done-reversible');
+      doneBtn.classList.remove('hidden');
+    } else {
+      row.parentNode.removeChild(row);
+    }
+  }
+
+  updateAllStateClicked(e) {
+    e.preventDefault();
+
+    const target = e.currentTarget;
+    const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
+    target.setAttribute('disabled', true);
+    target.classList.add('disabled');
+    $.ajax({
+      type: 'POST',
+      url: target.dataset.href,
+      dataType: 'json',
+      data: requestData,
+      success: (data) => {
+        this.updateAllState(target, data);
+        return this.updateBadges(data);
+      },
+    });
+  }
+
+  updateAllState(target, data) {
+    const markAllDoneBtn = document.querySelector('.js-todos-mark-all');
+    const undoAllBtn = document.querySelector('.js-todos-undo-all');
+    const todoListContainer = document.querySelector('.js-todos-list-container');
+    const nothingHereContainer = document.querySelector('.js-nothing-here-container');
+
+    target.removeAttribute('disabled');
+    target.classList.remove('disabled');
+
+    this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
+    undoAllBtn.classList.toggle('hidden');
+    markAllDoneBtn.classList.toggle('hidden');
+    todoListContainer.classList.toggle('hidden');
+    nothingHereContainer.classList.toggle('hidden');
+  }
+
+  updateBadges(data) {
+    $(document).trigger('todo:toggle', data.count);
+    document.querySelector('.todos-pending .badge').innerHTML = data.count;
+    document.querySelector('.todos-done .badge').innerHTML = data.done_count;
+  }
+
+  goToTodoUrl(e) {
+    const todoLink = this.dataset.url;
+
+    if (!todoLink) {
+      return;
+    }
+
+    if (gl.utils.isMetaClick(e)) {
+      const windowTarget = '_blank';
+      const selected = e.target;
+      e.preventDefault();
+
+      if (selected.tagName === 'IMG') {
+        const avatarUrl = selected.parentElement.getAttribute('href');
+        window.open(avatarUrl, windowTarget);
+      } else {
+        window.open(todoLink, windowTarget);
+      }
+    } else {
+      gl.utils.visitUrl(todoLink);
+    }
+  }
+}
+
+window.gl = window.gl || {};
+gl.Todos = Todos;
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
deleted file mode 100644
index e9513725d9d882b0270ccb613b00eea0aac639ca..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/todos.js.es6
+++ /dev/null
@@ -1,146 +0,0 @@
-/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
-/* global UsersSelect */
-
-((global) => {
-  class Todos {
-    constructor() {
-      this.initFilters();
-      this.bindEvents();
-
-      this.cleanupWrapper = this.cleanup.bind(this);
-      document.addEventListener('beforeunload', this.cleanupWrapper);
-    }
-
-    cleanup() {
-      this.unbindEvents();
-      document.removeEventListener('beforeunload', this.cleanupWrapper);
-    }
-
-    unbindEvents() {
-      $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
-      $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
-      $('.todo').off('click', this.goToTodoUrl);
-    }
-
-    bindEvents() {
-      this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
-      this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
-
-      $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
-      $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
-      $('.todo').on('click', this.goToTodoUrl);
-    }
-
-    initFilters() {
-      new UsersSelect();
-      this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
-      this.initFilterDropdown($('.js-type-search'), 'type');
-      this.initFilterDropdown($('.js-action-search'), 'action_id');
-
-      $('form.filter-form').on('submit', function (event) {
-        event.preventDefault();
-        gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
-      });
-    }
-
-    initFilterDropdown($dropdown, fieldName, searchFields) {
-      $dropdown.glDropdown({
-        fieldName,
-        selectable: true,
-        filterable: searchFields ? true : false,
-        search: { fields: searchFields },
-        data: $dropdown.data('data'),
-        clicked: function () {
-          return $dropdown.closest('form.filter-form').submit();
-        },
-      });
-    }
-
-    updateStateClicked(e) {
-      e.preventDefault();
-      const target = e.target;
-      target.setAttribute('disabled', '');
-      target.classList.add('disabled');
-      $.ajax({
-        type: 'POST',
-        url: target.getAttribute('href'),
-        dataType: 'json',
-        data: {
-          '_method': target.getAttribute('data-method'),
-        },
-        success: (data) => {
-          this.updateState(target);
-          this.updateBadges(data);
-        },
-      });
-    }
-
-    allDoneClicked(e) {
-      e.preventDefault();
-      const $target = $(e.currentTarget);
-      $target.disable();
-      $.ajax({
-        type: 'POST',
-        url: $target.attr('href'),
-        dataType: 'json',
-        data: {
-          '_method': 'delete',
-        },
-        success: (data) => {
-          $target.remove();
-          $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
-          this.updateBadges(data);
-        },
-      });
-    }
-
-    updateState(target) {
-      const row = target.closest('li');
-      const restoreBtn = row.querySelector('.js-undo-todo');
-      const doneBtn = row.querySelector('.js-done-todo');
-
-      target.removeAttribute('disabled');
-      target.classList.remove('disabled');
-      target.classList.add('hidden');
-
-      if (target === doneBtn) {
-        row.classList.add('done-reversible');
-        restoreBtn.classList.remove('hidden');
-      } else {
-        row.classList.remove('done-reversible');
-        doneBtn.classList.remove('hidden');
-      }
-    }
-
-    updateBadges(data) {
-      $(document).trigger('todo:toggle', data.count);
-      $('.todos-pending .badge').text(data.count);
-      $('.todos-done .badge').text(data.done_count);
-    }
-
-    goToTodoUrl(e) {
-      const todoLink = this.dataset.url;
-
-      if (!todoLink) {
-        return;
-      }
-
-      if (gl.utils.isMetaClick(e)) {
-        const windowTarget = '_blank';
-        const selected = e.target;
-        e.preventDefault();
-
-        if (selected.tagName === 'IMG') {
-          const avatarUrl = selected.parentElement.getAttribute('href');
-          window.open(avatarUrl, windowTarget);
-        } else {
-          window.open(todoLink, windowTarget);
-        }
-      } else {
-        gl.utils.visitUrl(todoLink);
-      }
-    }
-  }
-
-  global.Todos = Todos;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js
similarity index 100%
rename from app/assets/javascripts/u2f/authenticate.js.es6
rename to app/assets/javascripts/u2f/authenticate.js
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js
similarity index 100%
rename from app/assets/javascripts/user.js.es6
rename to app/assets/javascripts/user.js
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
new file mode 100644
index 0000000000000000000000000000000000000000..99419e85b205b35cf8a9093fde911f14c9ebc466
--- /dev/null
+++ b/app/assets/javascripts/user_callout.js
@@ -0,0 +1,60 @@
+/* global Cookies */
+
+const userCalloutElementName = '.user-callout';
+const closeButton = '.close-user-callout';
+const userCalloutBtn = '.user-callout-btn';
+const userCalloutSvgAttrName = 'callout-svg';
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+
+const USER_CALLOUT_TEMPLATE = `
+  <div class="bordered-box landing content-block">
+    <button class="btn btn-default close close-user-callout" type="button">
+      <i class="fa fa-times dismiss-icon"></i>
+    </button>
+    <div class="row">
+      <div class="col-sm-3 col-xs-12 svg-container">
+      </div>
+      <div class="col-sm-8 col-xs-12 inner-content">
+        <h4>
+          Customize your experience
+        </h4>
+        <p>
+          Change syntax themes, default project pages, and more in preferences.
+        </p>
+        <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
+      </div>
+  </div>
+</div>`;
+
+class UserCallout {
+  constructor() {
+    this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
+    this.userCalloutBody = $(userCalloutElementName);
+    this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
+    $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+    this.init();
+  }
+
+  init() {
+    const $template = $(USER_CALLOUT_TEMPLATE);
+    if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
+      $template.find('.svg-container').append(this.userCalloutSvg);
+      this.userCalloutBody.append($template);
+      $template.find(closeButton).on('click', e => this.dismissCallout(e));
+      $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
+    } else {
+      this.userCalloutBody.remove();
+    }
+  }
+
+  dismissCallout(e) {
+    Cookies.set(USER_CALLOUT_COOKIE, 'true');
+    const $currentTarget = $(e.currentTarget);
+    if ($currentTarget.hasClass('close-user-callout')) {
+      this.userCalloutBody.remove();
+    }
+  }
+}
+
+module.exports = UserCallout;
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js
similarity index 100%
rename from app/assets/javascripts/user_tabs.js.es6
rename to app/assets/javascripts/user_tabs.js
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js
similarity index 100%
rename from app/assets/javascripts/username_validator.js.es6
rename to app/assets/javascripts/username_validator.js
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 5111b260e1c9f0565bda95429bc0f800fb914b89..754d448564fef8506c1dc1876d9c01bcfbbe613d 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,5 +1,6 @@
 /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, 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, max-len */
-/* global d3 */
+
+import d3 from 'd3';
 
 (function() {
   var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 4cad60a59b10650feb39f8fa2e3ef889f77a7d3e..580e2d84be58d9dc8d96e128e1aa5796b75cf916 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,3 +1 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
+require('./calendar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index de33a31b4110fcc9b9edcea0206d4d20a6521b41..eb897e9dfe9acfdca59e2dc0529789704ccb3f44 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -53,13 +53,22 @@
           $loading = $block.find('.block-loading').fadeOut();
 
           var updateIssueBoardsIssue = function () {
-            $loading.fadeIn();
+            $loading.removeClass('hidden').fadeIn();
             gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
               .then(function () {
                 $loading.fadeOut();
               });
           };
 
+          $('.assign-to-me-link').on('click', (e) => {
+            e.preventDefault();
+            $(e.currentTarget).hide();
+            const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
+            $input.val(gon.current_user_id);
+            selectedId = $input.val();
+            $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
+          });
+
           $block.on('click', '.js-assign-yourself', function(e) {
             e.preventDefault();
 
@@ -81,7 +90,7 @@
             data = {};
             data[abilityName] = {};
             data[abilityName].assignee_id = selected != null ? selected : null;
-            $loading.fadeIn();
+            $loading.removeClass('hidden').fadeIn();
             $dropdown.trigger('loading.gl.dropdown');
             return $.ajax({
               type: 'PUT',
@@ -199,15 +208,15 @@
               if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
                 e.preventDefault();
                 selectedId = user.id;
+                if (selectedId === gon.current_user_id) {
+                  $('.assign-to-me-link').hide();
+                } else {
+                  $('.assign-to-me-link').show();
+                }
                 return;
               }
               if ($el.closest('.add-issues-modal').length) {
                 gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
-              } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
-                selectedId = user.id;
-                gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
-                gl.issueBoards.BoardsStore.updateFiltersUrl();
-                e.preventDefault();
               } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
                 selectedId = user.id;
                 return Issuable.filterResults($dropdown.closest('form'));
@@ -234,11 +243,16 @@
             id: function (user) {
               return user.id;
             },
+            opened: function(e) {
+              const $el = $(e.currentTarget);
+              $el.find('.is-active').removeClass('is-active');
+              $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
+            },
             renderRow: function(user) {
               var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
               username = user.username ? "@" + user.username : "";
               avatar = user.avatar_url ? user.avatar_url : false;
-              selected = user.id === selectedId ? "is-active" : "";
+              selected = user.id === parseInt(selectedId, 10) ? "is-active" : "";
               img = "";
               if (user.beforeDivider != null) {
                 "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>";
@@ -248,7 +262,7 @@
                 }
               }
               // split into three parts so we can remove the username section if nessesary
-              listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
+              listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
               listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
               listClosingTags = "</a> </li>";
               if (username === '') {
diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4f716acb72e64b3fbfa564073d671261571d510
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js
@@ -0,0 +1,10 @@
+class VersionCheckImage {
+  static bindErrorEvent(imageElement) {
+    imageElement.off('error').on('error', () => imageElement.hide());
+  }
+}
+
+window.gl = window.gl || {};
+gl.VersionCheckImage = VersionCheckImage;
+
+module.exports = VersionCheckImage;
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
deleted file mode 100644
index 1fa2b5ac3995ab1def54eda8755d167cf762aca1..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/version_check_image.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-(() => {
-  class VersionCheckImage {
-    static bindErrorEvent(imageElement) {
-      imageElement.off('error').on('error', () => imageElement.hide());
-    }
-  }
-
-  window.gl = window.gl || {};
-  gl.VersionCheckImage = VersionCheckImage;
-})();
diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js
similarity index 100%
rename from app/assets/javascripts/visibility_select.js.es6
rename to app/assets/javascripts/visibility_select.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 0000000000000000000000000000000000000000..aaebf29d8ae3cbfe463a346f88ddf5e3e51dec80
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    endpoint: {
+      type: String,
+      required: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+
+    title: {
+      type: String,
+      required: true,
+    },
+
+    icon: {
+      type: String,
+      required: true,
+    },
+
+    cssClass: {
+      type: String,
+      required: true,
+    },
+
+    confirmActionMessage: {
+      type: String,
+      required: false,
+    },
+  },
+
+  data() {
+    return {
+      isLoading: false,
+    };
+  },
+
+  computed: {
+    iconClass() {
+      return `fa fa-${this.icon}`;
+    },
+
+    buttonClass() {
+      return `btn has-tooltip ${this.cssClass}`;
+    },
+  },
+
+  methods: {
+    onClick() {
+      if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+        this.makeRequest();
+      } else if (!this.confirmActionMessage) {
+        this.makeRequest();
+      }
+    },
+
+    makeRequest() {
+      this.isLoading = true;
+
+      this.service.postAction(this.endpoint)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshPipelines');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <button
+      type="button"
+      @click="onClick"
+      :class="buttonClass"
+      :title="title"
+      :aria-label="title"
+      data-placement="top"
+      :disabled="isLoading">
+      <i :class="iconClass" aria-hidden="true"/>
+      <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+    </button>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e183d5c8eca1400f87745c7a43ecb6296c90694
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+  props: [
+    'pipeline',
+  ],
+  computed: {
+    user() {
+      return !!this.pipeline.user;
+    },
+  },
+  template: `
+    <td>
+      <a
+        :href="pipeline.path"
+        class="js-pipeline-url-link">
+        <span class="pipeline-id">#{{pipeline.id}}</span>
+      </a>
+      <span>by</span>
+      <a
+        class="js-pipeline-url-user"
+        v-if="user"
+        :href="pipeline.user.web_url">
+        <img
+          v-if="user"
+          class="avatar has-tooltip s20 "
+          :title="pipeline.user.name"
+          data-container="body"
+          :src="pipeline.user.avatar_url"
+        >
+      </a>
+      <span
+        v-if="!user"
+        class="js-pipeline-url-api api monospace">
+        API
+      </span>
+      <span
+        v-if="pipeline.flags.latest"
+        class="js-pipeline-url-lastest label label-success has-tooltip"
+        title="Latest pipeline for this branch"
+        data-original-title="Latest pipeline for this branch">
+        latest
+      </span>
+      <span
+        v-if="pipeline.flags.yaml_errors"
+        class="js-pipeline-url-yaml label label-danger has-tooltip"
+        :title="pipeline.yaml_errors"
+        :data-original-title="pipeline.yaml_errors">
+        yaml invalid
+      </span>
+      <span
+        v-if="pipeline.flags.stuck"
+        class="js-pipeline-url-stuck label label-warning">
+        stuck
+      </span>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..4bb2b04888460d24e8d3b74a95580c9e5c9cdf8a
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    actions: {
+      type: Array,
+      required: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      playIconSvg,
+      isLoading: false,
+    };
+  },
+
+  methods: {
+    onClickAction(endpoint) {
+      this.isLoading = true;
+
+      this.service.postAction(endpoint)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshPipelines');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <div class="btn-group" v-if="actions">
+      <button
+        type="button"
+        class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+        title="Manual job"
+        data-toggle="dropdown"
+        data-placement="top"
+        aria-label="Manual job"
+        :disabled="isLoading">
+        ${playIconSvg}
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+        <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      </button>
+
+      <ul class="dropdown-menu dropdown-menu-align-right">
+        <li v-for="action in actions">
+          <button
+            type="button"
+            class="js-pipeline-action-link no-btn"
+            @click="onClickAction(action.path)">
+            ${playIconSvg}
+            <span>{{action.name}}</span>
+          </button>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 0000000000000000000000000000000000000000..3555040d60f754b561a49508e6b77a722529e64f
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,32 @@
+export default {
+  props: {
+    artifacts: {
+      type: Array,
+      required: true,
+    },
+  },
+
+  template: `
+    <div class="btn-group" role="group">
+      <button
+        class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+        title="Artifacts"
+        data-placement="top"
+        data-toggle="dropdown"
+        aria-label="Artifacts">
+        <i class="fa fa-download" aria-hidden="true"></i>
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-align-right">
+        <li v-for="artifact in artifacts">
+          <a
+            rel="nofollow"
+            :href="artifact.path">
+            <i class="fa fa-download" aria-hidden="true"></i>
+            <span>Download {{artifact.name}} artifacts</span>
+          </a>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2c29002707005a2fd35c5c5f1767e02645c9767
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+  data() {
+    const svgsDictionary = {
+      icon_status_canceled: canceledSvg,
+      icon_status_created: createdSvg,
+      icon_status_failed: failedSvg,
+      icon_status_manual: manualSvg,
+      icon_status_pending: pendingSvg,
+      icon_status_running: runningSvg,
+      icon_status_skipped: skippedSvg,
+      icon_status_success: successSvg,
+      icon_status_warning: warningSvg,
+    };
+
+    return {
+      builds: '',
+      spinner: '<span class="fa fa-spinner fa-spin"></span>',
+      svg: svgsDictionary[this.stage.status.icon],
+    };
+  },
+
+  props: {
+    stage: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  updated() {
+    if (this.builds) {
+      this.stopDropdownClickPropagation();
+    }
+  },
+
+  methods: {
+    fetchBuilds(e) {
+      const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+      if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+      return this.$http.get(this.stage.dropdown_path)
+        .then((response) => {
+          this.builds = JSON.parse(response.body).html;
+        }, () => {
+          const flash = new Flash('Something went wrong on our end.');
+          return flash;
+        });
+    },
+
+    /**
+     * When the user right clicks or cmd/ctrl + click in the job name
+     * the dropdown should not be closed and the link should open in another tab,
+     * so we stop propagation of the click event inside the dropdown.
+     *
+     * Since this component is rendered multiple times per page we need to guarantee we only
+     * target the click event of this component.
+     */
+    stopDropdownClickPropagation() {
+      $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+        e.stopPropagation();
+      });
+    },
+  },
+  computed: {
+    buildsOrSpinner() {
+      return this.builds ? this.builds : this.spinner;
+    },
+    dropdownClass() {
+      if (this.builds) return 'js-builds-dropdown-container';
+      return 'js-builds-dropdown-loading builds-dropdown-loading';
+    },
+    buildStatus() {
+      return `Build: ${this.stage.status.label}`;
+    },
+    tooltip() {
+      return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+    },
+    triggerButtonClass() {
+      return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+    },
+  },
+  template: `
+    <div>
+      <button
+        @click="fetchBuilds($event)"
+        :class="triggerButtonClass"
+        :title="stage.title"
+        data-placement="top"
+        data-toggle="dropdown"
+        type="button"
+        :aria-label="stage.title">
+        <span v-html="svg" aria-hidden="true"></span>
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+      </button>
+      <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+        <div class="arrow-up" aria-hidden="true"></div>
+        <div
+          :class="dropdownClass"
+          class="js-builds-dropdown-list scrollable-menu"
+          v-html="buildsOrSpinner">
+        </div>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 0000000000000000000000000000000000000000..21a281af438ddaeafd95cdbebd2bfc5156a45d48
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+  props: {
+    pipeline: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    const svgsDictionary = {
+      icon_status_canceled: canceledSvg,
+      icon_status_created: createdSvg,
+      icon_status_failed: failedSvg,
+      icon_status_manual: manualSvg,
+      icon_status_pending: pendingSvg,
+      icon_status_running: runningSvg,
+      icon_status_skipped: skippedSvg,
+      icon_status_success: successSvg,
+      icon_status_warning: warningSvg,
+    };
+
+    return {
+      svg: svgsDictionary[this.pipeline.details.status.icon],
+    };
+  },
+
+  computed: {
+    cssClasses() {
+      return `ci-status ci-${this.pipeline.details.status.group}`;
+    },
+
+    detailsPath() {
+      const { status } = this.pipeline.details;
+      return status.has_details ? status.details_path : false;
+    },
+
+    content() {
+      return `${this.svg} ${this.pipeline.details.status.text}`;
+    },
+  },
+  template: `
+    <td class="commit-link">
+      <a
+        :class="cssClasses"
+        :href="detailsPath"
+        v-html="content">
+      </a>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 0000000000000000000000000000000000000000..498d0715f54657320ce8e6c7705c218c679b6324
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+  data() {
+    return {
+      currentTime: new Date(),
+      iconTimerSvg,
+    };
+  },
+  props: ['pipeline'],
+  computed: {
+    timeAgo() {
+      return gl.utils.getTimeago();
+    },
+    localTimeFinished() {
+      return gl.utils.formatDate(this.pipeline.details.finished_at);
+    },
+    timeStopped() {
+      const changeTime = this.currentTime;
+      const options = {
+        weekday: 'long',
+        year: 'numeric',
+        month: 'short',
+        day: 'numeric',
+      };
+      options.timeZoneName = 'short';
+      const finished = this.pipeline.details.finished_at;
+      if (!finished && changeTime) return false;
+      return ({ words: this.timeAgo.format(finished) });
+    },
+    duration() {
+      const { duration } = this.pipeline.details;
+      const date = new Date(duration * 1000);
+
+      let hh = date.getUTCHours();
+      let mm = date.getUTCMinutes();
+      let ss = date.getSeconds();
+
+      if (hh < 10) hh = `0${hh}`;
+      if (mm < 10) mm = `0${mm}`;
+      if (ss < 10) ss = `0${ss}`;
+
+      if (duration !== null) return `${hh}:${mm}:${ss}`;
+      return false;
+    },
+  },
+  methods: {
+    changeTime() {
+      this.currentTime = new Date();
+    },
+  },
+  template: `
+    <td class="pipelines-time-ago">
+      <p class="duration" v-if='duration'>
+        <span v-html="iconTimerSvg"></span>
+        {{duration}}
+      </p>
+      <p class="finished-at" v-if='timeStopped'>
+        <i class="fa fa-calendar"></i>
+        <time
+          data-toggle="tooltip"
+          data-placement="top"
+          data-container="body"
+          :data-original-title='localTimeFinished'>
+          {{timeStopped.words}}
+        </time>
+      </p>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4e2d3a1143dc35d567dc8a0e3e721a7c5663ccc
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -0,0 +1,28 @@
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+
+$(() => new Vue({
+  el: document.querySelector('.vue-pipelines-index'),
+
+  data() {
+    const project = document.querySelector('.pipelines');
+    const store = new PipelinesStore();
+
+    return {
+      store,
+      endpoint: project.dataset.url,
+    };
+  },
+  components: {
+    'vue-pipelines': PipelinesComponent,
+  },
+  template: `
+    <vue-pipelines
+      :endpoint="endpoint"
+      :store="store" />
+  `,
+}));
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
deleted file mode 100644
index e7432afb56e6a8668773e9e97467dad0e82b76b2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ /dev/null
@@ -1,36 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
-
-$(() => new Vue({
-  el: document.querySelector('.vue-pipelines-index'),
-
-  data() {
-    const project = document.querySelector('.pipelines');
-    const svgs = document.querySelector('.pipeline-svgs').dataset;
-
-    // Transform svgs DOMStringMap to a plain Object.
-    const svgsObject = gl.utils.DOMStringMapToObject(svgs);
-
-    return {
-      scope: project.dataset.url,
-      store: new gl.PipelineStore(),
-      svgs: svgsObject,
-    };
-  },
-  components: {
-    'vue-pipelines': gl.VuePipelines,
-  },
-  template: `
-    <vue-pipelines
-      :scope='scope'
-      :store='store'
-      :svgs='svgs'
-    >
-    </vue-pipelines>
-  `,
-}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
deleted file mode 100644
index 54e8f977a47d4230768c5704b22d577e4df6fbb6..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ /dev/null
@@ -1,105 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  gl.VuePipelineActions = Vue.extend({
-    props: ['pipeline', 'svgs'],
-    computed: {
-      actions() {
-        return this.pipeline.details.manual_actions.length > 0;
-      },
-      artifacts() {
-        return this.pipeline.details.artifacts.length > 0;
-      },
-    },
-    methods: {
-      download(name) {
-        return `Download ${name} artifacts`;
-      },
-    },
-    template: `
-      <td class="pipeline-actions hidden-xs">
-        <div class="controls pull-right">
-          <div class="btn-group inline">
-            <div class="btn-group">
-              <button
-                v-if='actions'
-                class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
-                data-toggle="dropdown"
-                title="Manual job"
-                data-placement="top"
-                aria-label="Manual job"
-              >
-                <span v-html='svgs.iconPlay' aria-hidden="true"></span>
-                <i class="fa fa-caret-down" aria-hidden="true"></i>
-              </button>
-              <ul class="dropdown-menu dropdown-menu-align-right">
-                <li v-for='action in pipeline.details.manual_actions'>
-                  <a
-                    rel="nofollow"
-                    data-method="post"
-                    :href='action.path'
-                  >
-                    <span v-html='svgs.iconPlay' aria-hidden="true"></span>
-                    <span>{{action.name}}</span>
-                  </a>
-                </li>
-              </ul>
-            </div>
-            <div class="btn-group">
-              <button
-                v-if='artifacts'
-                class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
-                title="Artifacts"
-                data-placement="top"
-                data-toggle="dropdown"
-                aria-label="Artifacts"
-              >
-                <i class="fa fa-download" aria-hidden="true"></i>
-                <i class="fa fa-caret-down" aria-hidden="true"></i>
-              </button>
-              <ul class="dropdown-menu dropdown-menu-align-right">
-                <li v-for='artifact in pipeline.details.artifacts'>
-                  <a
-                    rel="nofollow"
-                    download
-                    :href='artifact.path'
-                  >
-                    <i class="fa fa-download" aria-hidden="true"></i>
-                    <span>{{download(artifact.name)}}</span>
-                  </a>
-                </li>
-              </ul>
-            </div>
-          </div>
-          <div class="cancel-retry-btns inline">
-            <a
-              v-if='pipeline.flags.retryable'
-              class="btn has-tooltip"
-              title="Retry"
-              rel="nofollow"
-              data-method="post"
-              data-placement="top"
-              data-toggle="dropdown"
-              :href='pipeline.retry_path'
-              aria-label="Retry">
-              <i class="fa fa-repeat" aria-hidden="true"></i>
-            </a>
-            <a
-              v-if='pipeline.flags.cancelable'
-              class="btn btn-remove has-tooltip"
-              title="Cancel"
-              rel="nofollow"
-              data-method="post"
-              data-placement="top"
-              data-toggle="dropdown"
-              :href='pipeline.cancel_path'
-              aria-label="Cancel">
-              <i class="fa fa-remove" aria-hidden="true"></i>
-            </a>
-          </div>
-        </div>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
deleted file mode 100644
index ae5649f0519d67e477c2e08633093bf200ddfb8b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  gl.VuePipelineUrl = Vue.extend({
-    props: [
-      'pipeline',
-    ],
-    computed: {
-      user() {
-        return !!this.pipeline.user;
-      },
-    },
-    template: `
-      <td>
-        <a :href='pipeline.path'>
-          <span class="pipeline-id">#{{pipeline.id}}</span>
-        </a>
-        <span>by</span>
-        <a
-          v-if='user'
-          :href='pipeline.user.web_url'
-        >
-          <img
-            v-if='user'
-            class="avatar has-tooltip s20 "
-            :title='pipeline.user.name'
-            data-container="body"
-            :src='pipeline.user.avatar_url'
-          >
-        </a>
-        <span
-          v-if='!user'
-          class="api monospace"
-        >
-          API
-        </span>
-        <span
-          v-if='pipeline.flags.latest'
-          class="label label-success has-tooltip"
-          title="Latest pipeline for this branch"
-          data-original-title="Latest pipeline for this branch"
-        >
-          latest
-        </span>
-        <span
-          v-if='pipeline.flags.yaml_errors'
-          class="label label-danger has-tooltip"
-          :title='pipeline.yaml_errors'
-          :data-original-title='pipeline.yaml_errors'
-        >
-          yaml invalid
-        </span>
-        <span
-          v-if='pipeline.flags.stuck'
-          class="label label-warning"
-        >
-          stuck
-        </span>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
new file mode 100644
index 0000000000000000000000000000000000000000..f389e5e495041a69f704768576e4fe14f52b2588
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -0,0 +1,121 @@
+/* global Flash */
+/* eslint-disable no-new */
+import '~/flash';
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
+
+export default {
+  props: {
+    endpoint: {
+      type: String,
+      required: true,
+    },
+
+    store: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  components: {
+    'gl-pagination': TablePaginationComponent,
+    'pipelines-table-component': PipelinesTableComponent,
+  },
+
+  data() {
+    return {
+      state: this.store.state,
+      apiScope: 'all',
+      pagenum: 1,
+      pageRequest: false,
+    };
+  },
+
+  created() {
+    this.service = new PipelinesService(this.endpoint);
+
+    this.fetchPipelines();
+
+    eventHub.$on('refreshPipelines', this.fetchPipelines);
+  },
+
+  beforeUpdate() {
+    if (this.state.pipelines.length && this.$children) {
+      this.store.startTimeAgoLoops.call(this, Vue);
+    }
+  },
+
+  beforeDestroyed() {
+    eventHub.$off('refreshPipelines');
+  },
+
+  methods: {
+    /**
+     * Will change the page number and update the URL.
+     *
+     * @param  {Number} pageNumber desired page to go to.
+     */
+    change(pageNumber) {
+      const param = gl.utils.setParamInURL('page', pageNumber);
+
+      gl.utils.visitUrl(param);
+      return param;
+    },
+
+    fetchPipelines() {
+      const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+      const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+      this.pageRequest = true;
+      return this.service.getPipelines(scope, pageNumber)
+        .then(resp => ({
+          headers: resp.headers,
+          body: resp.json(),
+        }))
+        .then((response) => {
+          this.store.storeCount(response.body.count);
+          this.store.storePipelines(response.body.pipelines);
+          this.store.storePagination(response.headers);
+        })
+        .then(() => {
+          this.pageRequest = false;
+        })
+        .catch(() => {
+          this.pageRequest = false;
+          new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+        });
+    },
+  },
+  template: `
+    <div>
+      <div class="pipelines realtime-loading" v-if="pageRequest">
+        <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      </div>
+
+      <div class="blank-state blank-state-no-icon"
+        v-if="!pageRequest && state.pipelines.length === 0">
+        <h2 class="blank-state-title js-blank-state-title">
+          No pipelines to show
+        </h2>
+      </div>
+
+      <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
+        <pipelines-table-component
+          :pipelines="state.pipelines"
+          :service="service"/>
+      </div>
+
+      <gl-pagination
+        v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
+        :pagenum="pagenum"
+        :change="change"
+        :count="state.count.all"
+        :pageInfo="state.pageInfo"
+      >
+      </gl-pagination>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
deleted file mode 100644
index 0265c00a414d958dde4aeee29bef29bdf2f9c47d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ /dev/null
@@ -1,93 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
-
-((gl) => {
-  gl.VuePipelines = Vue.extend({
-
-    components: {
-      'gl-pagination': gl.VueGlPagination,
-      'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
-    },
-
-    data() {
-      return {
-        pipelines: [],
-        timeLoopInterval: '',
-        intervalId: '',
-        apiScope: 'all',
-        pageInfo: {},
-        pagenum: 1,
-        count: { all: 0, running_or_pending: 0 },
-        pageRequest: false,
-      };
-    },
-    props: ['scope', 'store', 'svgs'],
-    created() {
-      const pagenum = gl.utils.getParameterByName('page');
-      const scope = gl.utils.getParameterByName('scope');
-      if (pagenum) this.pagenum = pagenum;
-      if (scope) this.apiScope = scope;
-
-      this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
-    },
-
-    beforeUpdate() {
-      if (this.pipelines.length && this.$children) {
-        CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
-      }
-    },
-
-    methods: {
-      /**
-       * Changes the URL according to the pagination component.
-       *
-       * If no scope is provided, 'all' is assumed.
-       *
-       * Pagination component sends "null" when no scope is provided.
-       *
-       * @param  {Number} pagenum
-       * @param  {String} apiScope = 'all'
-       */
-      change(pagenum, apiScope) {
-        if (!apiScope) apiScope = 'all';
-        gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
-      },
-    },
-    template: `
-      <div>
-        <div class="pipelines realtime-loading" v-if='pageRequest'>
-          <i class="fa fa-spinner fa-spin"></i>
-        </div>
-
-        <div class="blank-state blank-state-no-icon"
-          v-if="!pageRequest && pipelines.length === 0">
-          <h2 class="blank-state-title js-blank-state-title">
-            No pipelines to show
-          </h2>
-        </div>
-
-        <div class="table-holder" v-if='!pageRequest && pipelines.length'>
-          <pipelines-table-component
-            :pipelines='pipelines'
-            :svgs='svgs'>
-          </pipelines-table-component>
-        </div>
-
-        <gl-pagination
-          v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
-          :pagenum='pagenum'
-          :change='change'
-          :count='count.all'
-          :pageInfo='pageInfo'
-        >
-        </gl-pagination>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 0000000000000000000000000000000000000000..708f5068dd30ad2b10660d070589c5eaca84b644
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+  /**
+  * Commits and merge request endpoints need to be requested with `.json`.
+  *
+  * The url provided to request the pipelines in the new merge request
+  * page already has `.json`.
+  *
+  * @param  {String} root
+  */
+  constructor(root) {
+    let endpoint;
+
+    if (root.indexOf('.json') === -1) {
+      endpoint = `${root}.json`;
+    } else {
+      endpoint = root;
+    }
+
+    this.pipelines = Vue.resource(endpoint);
+  }
+
+  getPipelines(scope, page) {
+    return this.pipelines.get({ scope, page });
+  }
+
+  /**
+   * Post request for all pipelines actions.
+   * Endpoint content type needs to be:
+   * `Content-Type:application/x-www-form-urlencoded`
+   *
+   * @param  {String} endpoint
+   * @return {Promise}
+   */
+  postAction(endpoint) {
+    return Vue.http.post(endpoint, {}, { emulateJSON: true });
+  }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
deleted file mode 100644
index 8cc417a9966120568f7f6b659f6ffcbe5b6c4a76..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ /dev/null
@@ -1,103 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  gl.VueStage = Vue.extend({
-    data() {
-      return {
-        builds: '',
-        spinner: '<span class="fa fa-spinner fa-spin"></span>',
-      };
-    },
-    props: {
-      stage: {
-        type: Object,
-        required: true,
-      },
-      svgs: {
-        type: Object,
-        required: true,
-      },
-      match: {
-        type: Function,
-        required: true,
-      },
-    },
-    methods: {
-      fetchBuilds(e) {
-        const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
-        if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
-        return this.$http.get(this.stage.dropdown_path)
-          .then((response) => {
-            this.builds = JSON.parse(response.body).html;
-          }, () => {
-            const flash = new Flash('Something went wrong on our end.');
-            return flash;
-          });
-      },
-      keepGraph(e) {
-        const { target } = e;
-
-        if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
-
-        if (
-          target.parentElement &&
-          (target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
-        ) return null;
-
-        return e.stopPropagation();
-      },
-    },
-    computed: {
-      buildsOrSpinner() {
-        return this.builds ? this.builds : this.spinner;
-      },
-      dropdownClass() {
-        if (this.builds) return 'js-builds-dropdown-container';
-        return 'js-builds-dropdown-loading builds-dropdown-loading';
-      },
-      buildStatus() {
-        return `Build: ${this.stage.status.label}`;
-      },
-      tooltip() {
-        return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
-      },
-      svg() {
-        const { icon } = this.stage.status;
-        const stageIcon = icon.replace(/icon/i, 'stage_icon');
-        return this.svgs[this.match(stageIcon)];
-      },
-      triggerButtonClass() {
-        return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
-      },
-    },
-    template: `
-      <div>
-        <button
-          @click='fetchBuilds($event)'
-          :class="triggerButtonClass"
-          :title='stage.title'
-          data-placement="top"
-          data-toggle="dropdown"
-          type="button"
-          :aria-label='stage.title'
-        >
-          <span v-html="svg" aria-hidden="true"></span>
-          <i class="fa fa-caret-down" aria-hidden="true"></i>
-        </button>
-        <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
-          <div class="arrow-up" aria-hidden="true"></div>
-          <div
-            @click='keepGraph($event)'
-            :class="dropdownClass"
-            class="js-builds-dropdown-list scrollable-menu"
-            v-html="buildsOrSpinner"
-          >
-          </div>
-        </ul>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6
deleted file mode 100644
index 05175082704f445618ae124a18ae4fb9cc2a092b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js.es6
+++ /dev/null
@@ -1,34 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  gl.VueStatusScope = Vue.extend({
-    props: [
-      'pipeline', 'svgs', 'match',
-    ],
-    computed: {
-      cssClasses() {
-        const cssObject = { 'ci-status': true };
-        cssObject[`ci-${this.pipeline.details.status.group}`] = true;
-        return cssObject;
-      },
-      svg() {
-        return this.svgs[this.match(this.pipeline.details.status.icon)];
-      },
-      detailsPath() {
-        const { status } = this.pipeline.details;
-        return status.has_details ? status.details_path : false;
-      },
-    },
-    template: `
-      <td class="commit-link">
-        <a
-          :class='cssClasses'
-          :href='detailsPath'
-          v-html='svg + pipeline.details.status.text'
-        >
-        </a>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
deleted file mode 100644
index 909007267b952ee3fbf7cf18273bee8df7d43c3f..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  const pageValues = (headers) => {
-    const normalized = gl.utils.normalizeHeaders(headers);
-    const paginationInfo = gl.utils.parseIntPagination(normalized);
-    return paginationInfo;
-  };
-
-  gl.PipelineStore = class {
-    fetchDataLoop(Vue, pageNum, url, apiScope) {
-      this.pageRequest = true;
-
-      return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
-      .then((response) => {
-        const pageInfo = pageValues(response.headers);
-        this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
-        const res = JSON.parse(response.body);
-        this.count = Object.assign({}, this.count, res.count);
-        this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
-        this.pageRequest = false;
-      }, () => {
-        this.pageRequest = false;
-        return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
-      });
-    }
-  };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
similarity index 62%
rename from app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
rename to app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index f1b80e45444568616f02b8c805dd3d1b3e7b11e0..7ac10086a55cac7b35ff13fda90993c2d649c1c5 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,31 +1,46 @@
 /* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
   constructor() {
     this.state = {};
+
     this.state.pipelines = [];
+    this.state.count = {};
+    this.state.pageInfo = {};
   }
 
   storePipelines(pipelines = []) {
     this.state.pipelines = pipelines;
+  }
 
-    return pipelines;
+  storeCount(count = {}) {
+    this.state.count = count;
+  }
+
+  storePagination(pagination = {}) {
+    let paginationInfo;
+
+    if (Object.keys(pagination).length) {
+      const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+      paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+    } else {
+      paginationInfo = pagination;
+    }
+
+    this.state.pageInfo = paginationInfo;
   }
 
   /**
+   * FIXME: Move this inside the component.
+   *
    * Once the data is received we will start the time ago loops.
    *
    * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
    * update the time to show how long as passed.
    *
    */
-  static startTimeAgoLoops() {
+  startTimeAgoLoops() {
     const startTimeLoops = () => {
       this.timeLoopInterval = setInterval(() => {
         this.$children[0].$children.reduce((acc, component) => {
@@ -44,5 +59,3 @@ class PipelinesStore {
     gl.VueRealtimeListener(removeIntervals, startIntervals);
   }
 }
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
deleted file mode 100644
index 3598da115739f31c0e533dd91ba7ebac6b95ada4..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ /dev/null
@@ -1,76 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-((gl) => {
-  gl.VueTimeAgo = Vue.extend({
-    data() {
-      return {
-        currentTime: new Date(),
-      };
-    },
-    props: ['pipeline', 'svgs'],
-    computed: {
-      timeAgo() {
-        return gl.utils.getTimeago();
-      },
-      localTimeFinished() {
-        return gl.utils.formatDate(this.pipeline.details.finished_at);
-      },
-      timeStopped() {
-        const changeTime = this.currentTime;
-        const options = {
-          weekday: 'long',
-          year: 'numeric',
-          month: 'short',
-          day: 'numeric',
-        };
-        options.timeZoneName = 'short';
-        const finished = this.pipeline.details.finished_at;
-        if (!finished && changeTime) return false;
-        return ({ words: this.timeAgo.format(finished) });
-      },
-      duration() {
-        const { duration } = this.pipeline.details;
-        const date = new Date(duration * 1000);
-
-        let hh = date.getUTCHours();
-        let mm = date.getUTCMinutes();
-        let ss = date.getSeconds();
-
-        if (hh < 10) hh = `0${hh}`;
-        if (mm < 10) mm = `0${mm}`;
-        if (ss < 10) ss = `0${ss}`;
-
-        if (duration !== null) return `${hh}:${mm}:${ss}`;
-        return false;
-      },
-    },
-    methods: {
-      changeTime() {
-        this.currentTime = new Date();
-      },
-    },
-    template: `
-      <td>
-        <p class="duration" v-if='duration'>
-          <span v-html='svgs.iconTimer'></span>
-          {{duration}}
-        </p>
-        <p class="finished-at" v-if='timeStopped'>
-          <i class="fa fa-calendar"></i>
-          <time
-            data-toggle="tooltip"
-            data-placement="top"
-            data-container="body"
-            :data-original-title='localTimeFinished'
-          >
-            {{timeStopped.words}}
-          </time>
-        </p>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js
similarity index 100%
rename from app/assets/javascripts/vue_realtime_listener/index.js.es6
rename to app/assets/javascripts/vue_realtime_listener/index.js
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
new file mode 100644
index 0000000000000000000000000000000000000000..fb68abd95a21825e32e8242de4c0ee419e6b26ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -0,0 +1,157 @@
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+  props: {
+    /**
+     * Indicates the existance of a tag.
+     * Used to render the correct icon, if true will render `fa-tag` icon,
+     * if false will render `fa-code-fork` icon.
+     */
+    tag: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+
+    /**
+     * If provided is used to render the branch name and url.
+     * Should contain the following properties:
+     * name
+     * ref_url
+     */
+    commitRef: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+
+    /**
+     * Used to link to the commit sha.
+     */
+    commitUrl: {
+      type: String,
+      required: false,
+      default: '',
+    },
+
+    /**
+     * Used to show the commit short sha that links to the commit url.
+     */
+    shortSha: {
+      type: String,
+      required: false,
+      default: '',
+    },
+
+    /**
+     * If provided shows the commit tile.
+     */
+    title: {
+      type: String,
+      required: false,
+      default: '',
+    },
+
+    /**
+     * If provided renders information about the author of the commit.
+     * When provided should include:
+     * `avatar_url` to render the avatar icon
+     * `web_url` to link to user profile
+     * `username` to render alt and title tags
+     */
+    author: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+  },
+
+  computed: {
+    /**
+     * Used to verify if all the properties needed to render the commit
+     * ref section were provided.
+     *
+     * TODO: Improve this! Use lodash _.has when we have it.
+     *
+     * @returns {Boolean}
+     */
+    hasCommitRef() {
+      return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+    },
+
+    /**
+     * Used to verify if all the properties needed to render the commit
+     * author section were provided.
+     *
+     * TODO: Improve this! Use lodash _.has when we have it.
+     *
+     * @returns {Boolean}
+     */
+    hasAuthor() {
+      return this.author &&
+        this.author.avatar_url &&
+        this.author.web_url &&
+        this.author.username;
+    },
+
+    /**
+     * If information about the author is provided will return a string
+     * to be rendered as the alt attribute of the img tag.
+     *
+     * @returns {String}
+     */
+    userImageAltDescription() {
+      return this.author &&
+        this.author.username ? `${this.author.username}'s avatar` : null;
+    },
+  },
+
+  data() {
+    return { commitIconSvg };
+  },
+
+  template: `
+    <div class="branch-commit">
+
+      <div v-if="hasCommitRef" class="icon-container">
+        <i v-if="tag" class="fa fa-tag"></i>
+        <i v-if="!tag" class="fa fa-code-fork"></i>
+      </div>
+
+      <a v-if="hasCommitRef"
+        class="monospace branch-name"
+        :href="commitRef.ref_url">
+        {{commitRef.name}}
+      </a>
+
+      <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+      <a class="commit-id monospace"
+        :href="commitUrl">
+        {{shortSha}}
+      </a>
+
+      <p class="commit-title">
+        <span v-if="title">
+          <a v-if="hasAuthor"
+            class="avatar-image-container"
+            :href="author.web_url">
+            <img
+              class="avatar has-tooltip s20"
+              :src="author.avatar_url"
+              :alt="userImageAltDescription"
+              :title="author.username" />
+          </a>
+
+          <a class="commit-row-message"
+            :href="commitUrl">
+            {{title}}
+          </a>
+        </span>
+        <span v-else>
+          Cant find HEAD commit for this branch
+        </span>
+      </p>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
deleted file mode 100644
index ff88e23682909b2aa9bf0c66f9fc4ecc8a0244b9..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/commit.js.es6
+++ /dev/null
@@ -1,164 +0,0 @@
-/* global Vue */
-window.Vue = require('vue');
-
-(() => {
-  window.gl = window.gl || {};
-
-  window.gl.CommitComponent = Vue.component('commit-component', {
-
-    props: {
-      /**
-       * Indicates the existance of a tag.
-       * Used to render the correct icon, if true will render `fa-tag` icon,
-       * if false will render `fa-code-fork` icon.
-       */
-      tag: {
-        type: Boolean,
-        required: false,
-        default: false,
-      },
-
-      /**
-       * If provided is used to render the branch name and url.
-       * Should contain the following properties:
-       * name
-       * ref_url
-       */
-      commitRef: {
-        type: Object,
-        required: false,
-        default: () => ({}),
-      },
-
-      /**
-       * Used to link to the commit sha.
-       */
-      commitUrl: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * Used to show the commit short sha that links to the commit url.
-       */
-      shortSha: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * If provided shows the commit tile.
-       */
-      title: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * If provided renders information about the author of the commit.
-       * When provided should include:
-       * `avatar_url` to render the avatar icon
-       * `web_url` to link to user profile
-       * `username` to render alt and title tags
-       */
-      author: {
-        type: Object,
-        required: false,
-        default: () => ({}),
-      },
-
-      commitIconSvg: {
-        type: String,
-        required: false,
-      },
-    },
-
-    computed: {
-      /**
-       * Used to verify if all the properties needed to render the commit
-       * ref section were provided.
-       *
-       * TODO: Improve this! Use lodash _.has when we have it.
-       *
-       * @returns {Boolean}
-       */
-      hasCommitRef() {
-        return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
-      },
-
-      /**
-       * Used to verify if all the properties needed to render the commit
-       * author section were provided.
-       *
-       * TODO: Improve this! Use lodash _.has when we have it.
-       *
-       * @returns {Boolean}
-       */
-      hasAuthor() {
-        return this.author &&
-          this.author.avatar_url &&
-          this.author.web_url &&
-          this.author.username;
-      },
-
-      /**
-       * If information about the author is provided will return a string
-       * to be rendered as the alt attribute of the img tag.
-       *
-       * @returns {String}
-       */
-      userImageAltDescription() {
-        return this.author &&
-          this.author.username ? `${this.author.username}'s avatar` : null;
-      },
-    },
-
-    template: `
-      <div class="branch-commit">
-
-        <div v-if="hasCommitRef" class="icon-container">
-          <i v-if="tag" class="fa fa-tag"></i>
-          <i v-if="!tag" class="fa fa-code-fork"></i>
-        </div>
-
-        <a v-if="hasCommitRef"
-          class="monospace branch-name"
-          :href="commitRef.ref_url">
-          {{commitRef.name}}
-        </a>
-
-        <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
-        <a class="commit-id monospace"
-          :href="commitUrl">
-          {{shortSha}}
-        </a>
-
-        <p class="commit-title">
-          <span v-if="title">
-            <a v-if="hasAuthor"
-              class="avatar-image-container"
-              :href="author.web_url">
-              <img
-                class="avatar has-tooltip s20"
-                :src="author.avatar_url"
-                :alt="userImageAltDescription"
-                :title="author.username" />
-            </a>
-
-            <a class="commit-row-message"
-              :href="commitUrl">
-              {{title}}
-            </a>
-          </span>
-          <span v-else>
-            Cant find HEAD commit for this branch
-          </span>
-        </p>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
new file mode 100644
index 0000000000000000000000000000000000000000..afd8d7acf6bddc59723b696bc4b0eb46a44cd9fb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -0,0 +1,48 @@
+import PipelinesTableRowComponent from './pipelines_table_row';
+
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+export default {
+  props: {
+    pipelines: {
+      type: Array,
+      required: true,
+      default: () => ([]),
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  components: {
+    'pipelines-table-row-component': PipelinesTableRowComponent,
+  },
+
+  template: `
+    <table class="table ci-table">
+      <thead>
+        <tr>
+          <th class="js-pipeline-status pipeline-status">Status</th>
+          <th class="js-pipeline-info pipeline-info">Pipeline</th>
+          <th class="js-pipeline-commit pipeline-commit">Commit</th>
+          <th class="js-pipeline-stages pipeline-stages">Stages</th>
+          <th class="js-pipeline-date pipeline-date"></th>
+          <th class="js-pipeline-actions pipeline-actions"></th>
+        </tr>
+      </thead>
+      <tbody>
+        <template v-for="model in pipelines"
+          v-bind:model="model">
+          <tr is="pipelines-table-row-component"
+            :pipeline="model"
+            :service="service"></tr>
+        </template>
+      </tbody>
+    </table>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
deleted file mode 100644
index 4bdaef31ee9a99e4325befc524f6fdd505253b8e..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('./pipelines_table_row');
-/**
- * Pipelines Table Component.
- *
- * Given an array of objects, renders a table.
- */
-
-(() => {
-  window.gl = window.gl || {};
-  gl.pipelines = gl.pipelines || {};
-
-  gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
-    props: {
-      pipelines: {
-        type: Array,
-        required: true,
-        default: () => ([]),
-      },
-
-      /**
-       * TODO: Remove this when we have webpack.
-       */
-      svgs: {
-        type: Object,
-        required: true,
-        default: () => ({}),
-      },
-    },
-
-    components: {
-      'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
-    },
-
-    template: `
-      <table class="table ci-table">
-        <thead>
-          <tr>
-            <th class="js-pipeline-status pipeline-status">Status</th>
-            <th class="js-pipeline-info pipeline-info">Pipeline</th>
-            <th class="js-pipeline-commit pipeline-commit">Commit</th>
-            <th class="js-pipeline-stages pipeline-stages">Stages</th>
-            <th class="js-pipeline-date pipeline-date"></th>
-            <th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
-          </tr>
-        </thead>
-        <tbody>
-          <template v-for="model in pipelines"
-            v-bind:model="model">
-            <tr is="pipelines-table-row-component"
-              :pipeline="model"
-              :svgs="svgs"></tr>
-          </template>
-        </tbody>
-      </table>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5b3cb9214e8b0554b2182625496bcff9b6a9e48
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -0,0 +1,228 @@
+/* eslint-disable no-param-reassign */
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+export default {
+  props: {
+    pipeline: {
+      type: Object,
+      required: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  components: {
+    'async-button-component': AsyncButtonComponent,
+    'pipelines-actions-component': PipelinesActionsComponent,
+    'pipelines-artifacts-component': PipelinesArtifactsComponent,
+    'commit-component': CommitComponent,
+    'dropdown-stage': PipelinesStageComponent,
+    'pipeline-url': PipelinesUrlComponent,
+    'status-scope': PipelinesStatusComponent,
+    'time-ago': PipelinesTimeagoComponent,
+  },
+
+  computed: {
+    /**
+     * If provided, returns the commit tag.
+     * Needed to render the commit component column.
+     *
+     * This field needs a lot of verification, because of different possible cases:
+     *
+     * 1. person who is an author of a commit might be a GitLab user
+     * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+     * 3. If GitLab user does not have avatar he/she might have a Gravatar
+     * 4. If committer is not a GitLab User he/she can have a Gravatar
+     * 5. We do not have consistent API object in this case
+     * 6. We should improve API and the code
+     *
+     * @returns {Object|Undefined}
+     */
+    commitAuthor() {
+      let commitAuthorInformation;
+
+      // 1. person who is an author of a commit might be a GitLab user
+      if (this.pipeline &&
+        this.pipeline.commit &&
+        this.pipeline.commit.author) {
+        // 2. if person who is an author of a commit is a GitLab user
+        // he/she can have a GitLab avatar
+        if (this.pipeline.commit.author.avatar_url) {
+          commitAuthorInformation = this.pipeline.commit.author;
+
+          // 3. If GitLab user does not have avatar he/she might have a Gravatar
+        } else if (this.pipeline.commit.author_gravatar_url) {
+          commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+            avatar_url: this.pipeline.commit.author_gravatar_url,
+          });
+        }
+      }
+
+      // 4. If committer is not a GitLab User he/she can have a Gravatar
+      if (this.pipeline &&
+        this.pipeline.commit) {
+        commitAuthorInformation = {
+          avatar_url: this.pipeline.commit.author_gravatar_url,
+          web_url: `mailto:${this.pipeline.commit.author_email}`,
+          username: this.pipeline.commit.author_name,
+        };
+      }
+
+      return commitAuthorInformation;
+    },
+
+    /**
+     * If provided, returns the commit tag.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitTag() {
+      if (this.pipeline.ref &&
+        this.pipeline.ref.tag) {
+        return this.pipeline.ref.tag;
+      }
+      return undefined;
+    },
+
+    /**
+     * If provided, returns the commit ref.
+     * Needed to render the commit component column.
+     *
+     * Matches `path` prop sent in the API to `ref_url` prop needed
+     * in the commit component.
+     *
+     * @returns {Object|Undefined}
+     */
+    commitRef() {
+      if (this.pipeline.ref) {
+        return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+          if (prop === 'path') {
+            accumulator.ref_url = this.pipeline.ref[prop];
+          } else {
+            accumulator[prop] = this.pipeline.ref[prop];
+          }
+          return accumulator;
+        }, {});
+      }
+
+      return undefined;
+    },
+
+    /**
+     * If provided, returns the commit url.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitUrl() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.commit_path) {
+        return this.pipeline.commit.commit_path;
+      }
+      return undefined;
+    },
+
+    /**
+     * If provided, returns the commit short sha.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitShortSha() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.short_id) {
+        return this.pipeline.commit.short_id;
+      }
+      return undefined;
+    },
+
+    /**
+     * If provided, returns the commit title.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitTitle() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.title) {
+        return this.pipeline.commit.title;
+      }
+      return undefined;
+    },
+  },
+
+  template: `
+    <tr class="commit">
+      <status-scope :pipeline="pipeline"/>
+
+      <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+      <td>
+        <commit-component
+          :tag="commitTag"
+          :commit-ref="commitRef"
+          :commit-url="commitUrl"
+          :short-sha="commitShortSha"
+          :title="commitTitle"
+          :author="commitAuthor"/>
+      </td>
+
+      <td class="stage-cell">
+        <div class="stage-container dropdown js-mini-pipeline-graph"
+          v-if="pipeline.details.stages.length > 0"
+          v-for="stage in pipeline.details.stages">
+          <dropdown-stage :stage="stage"/>
+        </div>
+      </td>
+
+      <time-ago :pipeline="pipeline"/>
+
+      <td class="pipeline-actions">
+        <div class="pull-right btn-group">
+          <pipelines-actions-component
+            v-if="pipeline.details.manual_actions.length"
+            :actions="pipeline.details.manual_actions"
+            :service="service" />
+
+          <pipelines-artifacts-component
+            v-if="pipeline.details.artifacts.length"
+            :artifacts="pipeline.details.artifacts" />
+
+          <async-button-component
+            v-if="pipeline.flags.retryable"
+            :service="service"
+            :endpoint="pipeline.retry_path"
+            css-class="js-pipelines-retry-button btn-default btn-retry"
+            title="Retry"
+            icon="repeat" />
+
+          <async-button-component
+            v-if="pipeline.flags.cancelable"
+            :service="service"
+            :endpoint="pipeline.cancel_path"
+            css-class="js-pipelines-cancel-button btn-remove"
+            title="Cancel"
+            icon="remove"
+            confirm-action-message="Are you sure you want to cancel this pipeline?" />
+        </div>
+      </td>
+    </tr>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
deleted file mode 100644
index 61c1b72d9d2549e1eb77d398dd9664df64699315..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
+++ /dev/null
@@ -1,234 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
-/**
- * Pipeline table row.
- *
- * Given the received object renders a table row in the pipelines' table.
- */
-(() => {
-  window.gl = window.gl || {};
-  gl.pipelines = gl.pipelines || {};
-
-  gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
-    props: {
-      pipeline: {
-        type: Object,
-        required: true,
-        default: () => ({}),
-      },
-
-      /**
-       * TODO: Remove this when we have webpack;
-       */
-      svgs: {
-        type: Object,
-        required: true,
-        default: () => ({}),
-      },
-    },
-
-    components: {
-      'commit-component': gl.CommitComponent,
-      'pipeline-actions': gl.VuePipelineActions,
-      'dropdown-stage': gl.VueStage,
-      'pipeline-url': gl.VuePipelineUrl,
-      'status-scope': gl.VueStatusScope,
-      'time-ago': gl.VueTimeAgo,
-    },
-
-    computed: {
-      /**
-       * If provided, returns the commit tag.
-       * Needed to render the commit component column.
-       *
-       * This field needs a lot of verification, because of different possible cases:
-       *
-       * 1. person who is an author of a commit might be a GitLab user
-       * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
-       * 3. If GitLab user does not have avatar he/she might have a Gravatar
-       * 4. If committer is not a GitLab User he/she can have a Gravatar
-       * 5. We do not have consistent API object in this case
-       * 6. We should improve API and the code
-       *
-       * @returns {Object|Undefined}
-       */
-      commitAuthor() {
-        let commitAuthorInformation;
-
-        // 1. person who is an author of a commit might be a GitLab user
-        if (this.pipeline &&
-          this.pipeline.commit &&
-          this.pipeline.commit.author) {
-          // 2. if person who is an author of a commit is a GitLab user
-          // he/she can have a GitLab avatar
-          if (this.pipeline.commit.author.avatar_url) {
-            commitAuthorInformation = this.pipeline.commit.author;
-
-            // 3. If GitLab user does not have avatar he/she might have a Gravatar
-          } else if (this.pipeline.commit.author_gravatar_url) {
-            commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
-              avatar_url: this.pipeline.commit.author_gravatar_url,
-            });
-          }
-        }
-
-        // 4. If committer is not a GitLab User he/she can have a Gravatar
-        if (this.pipeline &&
-          this.pipeline.commit) {
-          commitAuthorInformation = {
-            avatar_url: this.pipeline.commit.author_gravatar_url,
-            web_url: `mailto:${this.pipeline.commit.author_email}`,
-            username: this.pipeline.commit.author_name,
-          };
-        }
-
-        return commitAuthorInformation;
-      },
-
-      /**
-       * If provided, returns the commit tag.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitTag() {
-        if (this.pipeline.ref &&
-          this.pipeline.ref.tag) {
-          return this.pipeline.ref.tag;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit ref.
-       * Needed to render the commit component column.
-       *
-       * Matches `path` prop sent in the API to `ref_url` prop needed
-       * in the commit component.
-       *
-       * @returns {Object|Undefined}
-       */
-      commitRef() {
-        if (this.pipeline.ref) {
-          return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
-            if (prop === 'path') {
-              accumulator.ref_url = this.pipeline.ref[prop];
-            } else {
-              accumulator[prop] = this.pipeline.ref[prop];
-            }
-            return accumulator;
-          }, {});
-        }
-
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit url.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitUrl() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.commit_path) {
-          return this.pipeline.commit.commit_path;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit short sha.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitShortSha() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.short_id) {
-          return this.pipeline.commit.short_id;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit title.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitTitle() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.title) {
-          return this.pipeline.commit.title;
-        }
-        return undefined;
-      },
-    },
-
-    methods: {
-      /**
-       * FIXME: This should not be in this component but in the components that
-       * need this function.
-       *
-       * Used to render SVGs in the following components:
-       * - status-scope
-       * - dropdown-stage
-       *
-       * @param  {String} string
-       * @return {String}
-       */
-      match(string) {
-        return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
-      },
-    },
-
-    template: `
-      <tr class="commit">
-        <status-scope
-          :pipeline="pipeline"
-          :svgs="svgs"
-          :match="match">
-        </status-scope>
-
-        <pipeline-url :pipeline="pipeline"></pipeline-url>
-
-        <td>
-          <commit-component
-            :tag="commitTag"
-            :commit-ref="commitRef"
-            :commit-url="commitUrl"
-            :short-sha="commitShortSha"
-            :title="commitTitle"
-            :author="commitAuthor"
-            :commit-icon-svg="svgs.commitIconSvg">
-          </commit-component>
-        </td>
-
-        <td class="stage-cell">
-          <div class="stage-container dropdown js-mini-pipeline-graph"
-            v-if="pipeline.details.stages.length > 0"
-            v-for="stage in pipeline.details.stages">
-            <dropdown-stage
-              :stage="stage"
-              :svgs="svgs"
-              :match="match">
-            </dropdown-stage>
-          </div>
-        </td>
-
-        <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
-
-        <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
-      </tr>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9cd28f62493584bbe3c4df1b3f6f5802b561b66
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -0,0 +1,135 @@
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '<< First';
+const LAST = 'Last >>';
+
+export default {
+  props: {
+    /**
+      This function will take the information given by the pagination component
+
+      Here is an example `change` method:
+
+      change(pagenum) {
+        gl.utils.visitUrl(`?page=${pagenum}`);
+      },
+    */
+    change: {
+      type: Function,
+      required: true,
+    },
+
+    /**
+      pageInfo will come from the headers of the API call
+      in the `.then` clause of the VueResource API call
+      there should be a function that contructs the pageInfo for this component
+
+      This is an example:
+
+      const pageInfo = headers => ({
+        perPage: +headers['X-Per-Page'],
+        page: +headers['X-Page'],
+        total: +headers['X-Total'],
+        totalPages: +headers['X-Total-Pages'],
+        nextPage: +headers['X-Next-Page'],
+        previousPage: +headers['X-Prev-Page'],
+      });
+    */
+    pageInfo: {
+      type: Object,
+      required: true,
+    },
+  },
+  methods: {
+    changePage(e) {
+      const text = e.target.innerText;
+      const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+      switch (text) {
+        case SPREAD:
+          break;
+        case LAST:
+          this.change(totalPages);
+          break;
+        case NEXT:
+          this.change(nextPage);
+          break;
+        case PREV:
+          this.change(previousPage);
+          break;
+        case FIRST:
+          this.change(1);
+          break;
+        default:
+          this.change(+text);
+          break;
+      }
+    },
+  },
+  computed: {
+    prev() {
+      return this.pageInfo.previousPage;
+    },
+    next() {
+      return this.pageInfo.nextPage;
+    },
+    getItems() {
+      const total = this.pageInfo.totalPages;
+      const page = this.pageInfo.page;
+      const items = [];
+
+      if (page > 1) items.push({ title: FIRST });
+
+      if (page > 1) {
+        items.push({ title: PREV, prev: true });
+      } else {
+        items.push({ title: PREV, disabled: true, prev: true });
+      }
+
+      if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+      const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+      const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+      for (let i = start; i <= end; i += 1) {
+        const isActive = i === page;
+        items.push({ title: i, active: isActive, page: true });
+      }
+
+      if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+        items.push({ title: SPREAD, separator: true, page: true });
+      }
+
+      if (page === total) {
+        items.push({ title: NEXT, disabled: true, next: true });
+      } else if (total - page >= 1) {
+        items.push({ title: NEXT, next: true });
+      }
+
+      if (total - page >= 1) items.push({ title: LAST, last: true });
+
+      return items;
+    },
+  },
+  template: `
+    <div class="gl-pagination">
+      <ul class="pagination clearfix">
+        <li v-for='item in getItems'
+          :class='{
+            page: item.page,
+            prev: item.prev,
+            next: item.next,
+            separator: item.separator,
+            active: item.active,
+            disabled: item.disabled
+          }'
+        >
+          <a @click="changePage($event)">{{item.title}}</a>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
deleted file mode 100644
index d8042a9b7fc42cf87eec7f5f7cf7bf1251134ae6..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
+++ /dev/null
@@ -1,150 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
-  const PAGINATION_UI_BUTTON_LIMIT = 4;
-  const UI_LIMIT = 6;
-  const SPREAD = '...';
-  const PREV = 'Prev';
-  const NEXT = 'Next';
-  const FIRST = '<< First';
-  const LAST = 'Last >>';
-
-  gl.VueGlPagination = Vue.extend({
-    props: {
-
-      // TODO: Consider refactoring in light of turbolinks removal.
-
-      /**
-        This function will take the information given by the pagination component
-        And make a new Turbolinks call
-
-        Here is an example `change` method:
-
-        change(pagenum, apiScope) {
-          gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
-        },
-      */
-
-      change: {
-        type: Function,
-        required: true,
-      },
-
-      /**
-        pageInfo will come from the headers of the API call
-        in the `.then` clause of the VueResource API call
-        there should be a function that contructs the pageInfo for this component
-
-        This is an example:
-
-        const pageInfo = headers => ({
-          perPage: +headers['X-Per-Page'],
-          page: +headers['X-Page'],
-          total: +headers['X-Total'],
-          totalPages: +headers['X-Total-Pages'],
-          nextPage: +headers['X-Next-Page'],
-          previousPage: +headers['X-Prev-Page'],
-        });
-      */
-
-      pageInfo: {
-        type: Object,
-        required: true,
-      },
-    },
-    methods: {
-      changePage(e) {
-        const apiScope = gl.utils.getParameterByName('scope');
-
-        const text = e.target.innerText;
-        const { totalPages, nextPage, previousPage } = this.pageInfo;
-
-        switch (text) {
-          case SPREAD:
-            break;
-          case LAST:
-            this.change(totalPages, apiScope);
-            break;
-          case NEXT:
-            this.change(nextPage, apiScope);
-            break;
-          case PREV:
-            this.change(previousPage, apiScope);
-            break;
-          case FIRST:
-            this.change(1, apiScope);
-            break;
-          default:
-            this.change(+text, apiScope);
-            break;
-        }
-      },
-    },
-    computed: {
-      prev() {
-        return this.pageInfo.previousPage;
-      },
-      next() {
-        return this.pageInfo.nextPage;
-      },
-      getItems() {
-        const total = this.pageInfo.totalPages;
-        const page = this.pageInfo.page;
-        const items = [];
-
-        if (page > 1) items.push({ title: FIRST });
-
-        if (page > 1) {
-          items.push({ title: PREV, prev: true });
-        } else {
-          items.push({ title: PREV, disabled: true, prev: true });
-        }
-
-        if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
-
-        const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
-        const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
-
-        for (let i = start; i <= end; i++) {
-          const isActive = i === page;
-          items.push({ title: i, active: isActive, page: true });
-        }
-
-        if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
-          items.push({ title: SPREAD, separator: true, page: true });
-        }
-
-        if (page === total) {
-          items.push({ title: NEXT, disabled: true, next: true });
-        } else if (total - page >= 1) {
-          items.push({ title: NEXT, next: true });
-        }
-
-        if (total - page >= 1) items.push({ title: LAST, last: true });
-
-        return items;
-      },
-    },
-    template: `
-      <div class="gl-pagination">
-        <ul class="pagination clearfix">
-          <li v-for='item in getItems'
-            :class='{
-              page: item.page,
-              prev: item.prev,
-              next: item.next,
-              separator: item.separator,
-              active: item.active,
-              disabled: item.disabled
-            }'
-          >
-            <a @click="changePage($event)">{{item.title}}</a>
-          </li>
-        </ul>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
similarity index 59%
rename from app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
rename to app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index d3229f9f73002d45c116dee27760ce907907c336..f1c1e553b1669108d151ae716bda0e313f6bdc5d 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,15 +1,13 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+/* eslint-disable no-param-reassign, no-plusplus */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
 
 Vue.http.interceptors.push((request, next) => {
   Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
 
-  next((response) => {
-    if (typeof response.data === 'string') {
-      response.data = JSON.parse(response.data);
-    }
-
+  next(() => {
     Vue.activeResources--;
   });
 });
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js
similarity index 100%
rename from app/assets/javascripts/wikis.js.es6
rename to app/assets/javascripts/wikis.js
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 1dcd1f8a6fc32f89835d89e0a08a72b85a8152d5..83a8eeaafdeda103e5e184f9f0fb771eae0670bc 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
  * This is a manifest file that'll automatically include all the stylesheets available in this directory
  * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
  * the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery-ui/autocomplete
  *= require jquery.atwho
  *= require select2
  *= require_self
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 39cf3b5f8ae551fdb68803ef3a57f9bbcef62644..5bb7e8caec17af8202b6d0cbd0a912147087152b 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,5 +44,6 @@
 @import "framework/images.scss";
 @import "framework/broadcast-messages";
 @import "framework/emojis.scss";
+@import "framework/emoji-sprites.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 49907417e2699d7d0f29a26e0e296dcb5be8f030..546718ddaf8326a632d5e751da7913e97e4c1bed 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -7,6 +7,7 @@
 
 .emoji-menu {
   position: absolute;
+  top: 0;
   margin-top: 3px;
   padding: $gl-padding;
   z-index: 9;
@@ -20,7 +21,7 @@
   opacity: 0;
   transform: scale(.2);
   transform-origin: 0 -45px;
-  transition: .3s cubic-bezier(.87,-.41,.19,1.44);
+  transition: .3s cubic-bezier(.67,.06,.19,1.44);
   transition-property: transform, opacity;
 
   &.is-aligned-right {
@@ -47,12 +48,13 @@
 }
 
 .emoji-menu-list {
-  list-style: none;
-  padding-left: 0;
   margin-bottom: 0;
+  padding-left: 0;
+  list-style: none;
 }
 
 .emoji-menu-list-item {
+  float: left;
   padding: 3px;
   margin-left: 1px;
   margin-right: 1px;
@@ -94,7 +96,7 @@
 
 .award-control {
   margin: 3px 5px 3px 0;
-  padding: 5px 6px;
+  padding: .35em .4em;
   outline: 0;
 
   &.disabled {
@@ -136,10 +138,12 @@
   }
 
   .icon,
+  gl-emoji,
   .award-control-icon {
-    float: left;
-    margin-right: 5px;
-    font-size: 18px;
+    vertical-align: middle;
+    margin-right: 0.15em;
+    font-size: 1.5em;
+    line-height: 1;
   }
 
   .award-control-icon-loading {
@@ -150,4 +154,8 @@
     color: $border-gray-normal;
     margin-top: 1px;
   }
+
+  .award-control-text {
+    vertical-align: middle;
+  }
 }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0f9213b98e35bd1dd53de1a6ab8e057146d1f3f1..9a4129cdc8da3fec150b90be949db4002dd73ffb 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -229,7 +229,7 @@
   .controls {
     float: right;
     margin-top: 8px;
-    padding-bottom: 7px;
+    padding-bottom: 8px;
     border-bottom: 1px solid $border-color;
   }
 }
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index fb8ea18d1224c44ff68e4e031d021418139620b8..9a0f7a14e570627a75d15c470243cb07fe078904 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,6 +1,7 @@
 .calender-block {
   padding-left: 0;
   padding-right: 0;
+  border-top: 0;
   direction: rtl;
 
   @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbde89f07a9e7e35e75cf067353b41f7..2c33b235980b727b6553e5e44689587accf2cc51 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
     @include str-truncated(100%);
   }
 }
+
+.tooltip {
+  .tooltip-inner {
+    word-wrap: break-word;
+  }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ff31e7f7b3dcca45b13d893da1875bc98f8fae00..186bb9ac616b2dca0e9910c310f6ac8a5c6e3eb2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
   white-space: nowrap;
 
   &[disabled] {
-    background-color: $input-bg-disabled;
+    opacity: .65;
     cursor: not-allowed;
   }
 
@@ -96,7 +96,7 @@
 
 .dropdown-menu-toggle {
   @extend .dropdown-toggle;
-  padding-right: 20px;
+  padding-right: 25px;
   position: relative;
   width: 163px;
   text-overflow: ellipsis;
@@ -107,11 +107,12 @@
 
     &.fa-spinner {
       font-size: 16px;
-      margin-top: -8px;
+      margin-top: -3px;
     }
   }
 
-  .fa-chevron-down {
+  .fa-chevron-down,
+  .fa-spinner {
     position: absolute;
     top: 11px;
     right: 8px;
@@ -158,12 +159,12 @@
   li {
     text-align: left;
     list-style: none;
-    padding: 0 8px;
+    padding: 0 10px;
   }
 
   .divider {
     height: 1px;
-    margin: 8px;
+    margin: 6px 10px;
     padding: 0;
     background-color: $dropdown-divider-color;
   }
@@ -180,7 +181,7 @@
     display: block;
     position: relative;
     padding: 5px 8px;
-    color: $dropdown-link-color;
+    color: $gl-text-color;
     line-height: initial;
     text-overflow: ellipsis;
     border-radius: 2px;
@@ -192,6 +193,10 @@
     &.is-focused {
       background-color: $dropdown-link-hover-bg;
       text-decoration: none;
+
+      .badge {
+        background-color: darken($row-hover, 5%);
+      }
     }
 
     &.dropdown-menu-empty-link {
@@ -213,10 +218,12 @@
   }
 
   .dropdown-header {
-    color: $gl-text-color-secondary;
+    color: $gl-text-color;
     font-size: 13px;
+    font-weight: 600;
     line-height: 22px;
-    padding: 0 10px;
+    text-transform: capitalize;
+    padding: 0 16px;
   }
 
   .separator + .dropdown-header {
@@ -228,6 +235,12 @@
     padding: 5px 8px;
     color: $gl-text-color-secondary;
   }
+
+  .badge {
+    position: absolute;
+    right: 8px;
+    top: 5px;
+  }
 }
 
 .dropdown-menu-drop-up {
@@ -313,14 +326,17 @@
 
 .dropdown-menu-selectable {
   a {
-    padding-left: 25px;
+    padding-left: 26px;
 
     &.is-indeterminate,
     &.is-active {
+      font-weight: 600;
+      color: $gl-text-color;
+
       &::before {
         position: absolute;
-        left: 5px;
-        top: 8px;
+        left: 6px;
+        top: 6px;
         font: normal normal normal 14px/1 FontAwesome;
         font-size: inherit;
         text-rendering: auto;
@@ -342,7 +358,7 @@
 
 .dropdown-title {
   position: relative;
-  padding: 0 25px 10px;
+  padding: 2px 25px 10px;
   margin: 0 10px 10px;
   font-weight: 600;
   line-height: 1;
@@ -372,7 +388,7 @@
   right: 5px;
   width: 20px;
   height: 20px;
-  top: -3px;
+  top: -1px;
 }
 
 .dropdown-menu-back {
diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss
new file mode 100644
index 0000000000000000000000000000000000000000..925415f84b1951bcde0d709a53d50bf7a8cf4558
--- /dev/null
+++ b/app/assets/stylesheets/framework/emoji-sprites.scss
@@ -0,0 +1,1811 @@
+.emoji-zzz { background-position: 0 0; }
+.emoji-1234 { background-position: -20px 0; }
+.emoji-1F627 { background-position: 0 -20px; }
+.emoji-8ball { background-position: -20px -20px; }
+.emoji-a { background-position: -40px 0; }
+.emoji-ab { background-position: -40px -20px; }
+.emoji-abc { background-position: 0 -40px; }
+.emoji-abcd { background-position: -20px -40px; }
+.emoji-accept { background-position: -40px -40px; }
+.emoji-aerial_tramway { background-position: -60px 0; }
+.emoji-airplane { background-position: -60px -20px; }
+.emoji-airplane_arriving { background-position: -60px -40px; }
+.emoji-airplane_departure { background-position: 0 -60px; }
+.emoji-airplane_small { background-position: -20px -60px; }
+.emoji-alarm_clock { background-position: -40px -60px; }
+.emoji-alembic { background-position: -60px -60px; }
+.emoji-alien { background-position: -80px 0; }
+.emoji-ambulance { background-position: -80px -20px; }
+.emoji-amphora { background-position: -80px -40px; }
+.emoji-anchor { background-position: -80px -60px; }
+.emoji-angel { background-position: 0 -80px; }
+.emoji-angel_tone1 { background-position: -20px -80px; }
+.emoji-angel_tone2 { background-position: -40px -80px; }
+.emoji-angel_tone3 { background-position: -60px -80px; }
+.emoji-angel_tone4 { background-position: -80px -80px; }
+.emoji-angel_tone5 { background-position: -100px 0; }
+.emoji-anger { background-position: -100px -20px; }
+.emoji-anger_right { background-position: -100px -40px; }
+.emoji-angry { background-position: -100px -60px; }
+.emoji-ant { background-position: -100px -80px; }
+.emoji-apple { background-position: 0 -100px; }
+.emoji-aquarius { background-position: -20px -100px; }
+.emoji-aries { background-position: -40px -100px; }
+.emoji-arrow_backward { background-position: -60px -100px; }
+.emoji-arrow_double_down { background-position: -80px -100px; }
+.emoji-arrow_double_up { background-position: -100px -100px; }
+.emoji-arrow_down { background-position: -120px 0; }
+.emoji-arrow_down_small { background-position: -120px -20px; }
+.emoji-arrow_forward { background-position: -120px -40px; }
+.emoji-arrow_heading_down { background-position: -120px -60px; }
+.emoji-arrow_heading_up { background-position: -120px -80px; }
+.emoji-arrow_left { background-position: -120px -100px; }
+.emoji-arrow_lower_left { background-position: 0 -120px; }
+.emoji-arrow_lower_right { background-position: -20px -120px; }
+.emoji-arrow_right { background-position: -40px -120px; }
+.emoji-arrow_right_hook { background-position: -60px -120px; }
+.emoji-arrow_up { background-position: -80px -120px; }
+.emoji-arrow_up_down { background-position: -100px -120px; }
+.emoji-arrow_up_small { background-position: -120px -120px; }
+.emoji-arrow_upper_left { background-position: -140px 0; }
+.emoji-arrow_upper_right { background-position: -140px -20px; }
+.emoji-arrows_clockwise { background-position: -140px -40px; }
+.emoji-arrows_counterclockwise { background-position: -140px -60px; }
+.emoji-art { background-position: -140px -80px; }
+.emoji-articulated_lorry { background-position: -140px -100px; }
+.emoji-asterisk { background-position: -140px -120px; }
+.emoji-astonished { background-position: 0 -140px; }
+.emoji-athletic_shoe { background-position: -20px -140px; }
+.emoji-atm { background-position: -40px -140px; }
+.emoji-atom { background-position: -60px -140px; }
+.emoji-avocado { background-position: -80px -140px; }
+.emoji-b { background-position: -100px -140px; }
+.emoji-baby { background-position: -120px -140px; }
+.emoji-baby_bottle { background-position: -140px -140px; }
+.emoji-baby_chick { background-position: -160px 0; }
+.emoji-baby_symbol { background-position: -160px -20px; }
+.emoji-baby_tone1 { background-position: -160px -40px; }
+.emoji-baby_tone2 { background-position: -160px -60px; }
+.emoji-baby_tone3 { background-position: -160px -80px; }
+.emoji-baby_tone4 { background-position: -160px -100px; }
+.emoji-baby_tone5 { background-position: -160px -120px; }
+.emoji-back { background-position: -160px -140px; }
+.emoji-bacon { background-position: 0 -160px; }
+.emoji-badminton { background-position: -20px -160px; }
+.emoji-baggage_claim { background-position: -40px -160px; }
+.emoji-balloon { background-position: -60px -160px; }
+.emoji-ballot_box { background-position: -80px -160px; }
+.emoji-ballot_box_with_check { background-position: -100px -160px; }
+.emoji-bamboo { background-position: -120px -160px; }
+.emoji-banana { background-position: -140px -160px; }
+.emoji-bangbang { background-position: -160px -160px; }
+.emoji-bank { background-position: -180px 0; }
+.emoji-bar_chart { background-position: -180px -20px; }
+.emoji-barber { background-position: -180px -40px; }
+.emoji-baseball { background-position: -180px -60px; }
+.emoji-basketball { background-position: -180px -80px; }
+.emoji-basketball_player { background-position: -180px -100px; }
+.emoji-basketball_player_tone1 { background-position: -180px -120px; }
+.emoji-basketball_player_tone2 { background-position: -180px -140px; }
+.emoji-basketball_player_tone3 { background-position: -180px -160px; }
+.emoji-basketball_player_tone4 { background-position: 0 -180px; }
+.emoji-basketball_player_tone5 { background-position: -20px -180px; }
+.emoji-bat { background-position: -40px -180px; }
+.emoji-bath { background-position: -60px -180px; }
+.emoji-bath_tone1 { background-position: -80px -180px; }
+.emoji-bath_tone2 { background-position: -100px -180px; }
+.emoji-bath_tone3 { background-position: -120px -180px; }
+.emoji-bath_tone4 { background-position: -140px -180px; }
+.emoji-bath_tone5 { background-position: -160px -180px; }
+.emoji-bathtub { background-position: -180px -180px; }
+.emoji-battery { background-position: -200px 0; }
+.emoji-beach { background-position: -200px -20px; }
+.emoji-beach_umbrella { background-position: -200px -40px; }
+.emoji-bear { background-position: -200px -60px; }
+.emoji-bed { background-position: -200px -80px; }
+.emoji-bee { background-position: -200px -100px; }
+.emoji-beer { background-position: -200px -120px; }
+.emoji-beers { background-position: -200px -140px; }
+.emoji-beetle { background-position: -200px -160px; }
+.emoji-beginner { background-position: -200px -180px; }
+.emoji-bell { background-position: 0 -200px; }
+.emoji-bellhop { background-position: -20px -200px; }
+.emoji-bento { background-position: -40px -200px; }
+.emoji-bicyclist { background-position: -60px -200px; }
+.emoji-bicyclist_tone1 { background-position: -80px -200px; }
+.emoji-bicyclist_tone2 { background-position: -100px -200px; }
+.emoji-bicyclist_tone3 { background-position: -120px -200px; }
+.emoji-bicyclist_tone4 { background-position: -140px -200px; }
+.emoji-bicyclist_tone5 { background-position: -160px -200px; }
+.emoji-bike { background-position: -180px -200px; }
+.emoji-bikini { background-position: -200px -200px; }
+.emoji-biohazard { background-position: -220px 0; }
+.emoji-bird { background-position: -220px -20px; }
+.emoji-birthday { background-position: -220px -40px; }
+.emoji-black_circle { background-position: -220px -60px; }
+.emoji-black_heart { background-position: -220px -80px; }
+.emoji-black_joker { background-position: -220px -100px; }
+.emoji-black_large_square { background-position: -220px -120px; }
+.emoji-black_medium_small_square { background-position: -220px -140px; }
+.emoji-black_medium_square { background-position: -220px -160px; }
+.emoji-black_nib { background-position: -220px -180px; }
+.emoji-black_small_square { background-position: -220px -200px; }
+.emoji-black_square_button { background-position: 0 -220px; }
+.emoji-blossom { background-position: -20px -220px; }
+.emoji-blowfish { background-position: -40px -220px; }
+.emoji-blue_book { background-position: -60px -220px; }
+.emoji-blue_car { background-position: -80px -220px; }
+.emoji-blue_heart { background-position: -100px -220px; }
+.emoji-blush { background-position: -120px -220px; }
+.emoji-boar { background-position: -140px -220px; }
+.emoji-bomb { background-position: -160px -220px; }
+.emoji-book { background-position: -180px -220px; }
+.emoji-bookmark { background-position: -200px -220px; }
+.emoji-bookmark_tabs { background-position: -220px -220px; }
+.emoji-books { background-position: -240px 0; }
+.emoji-boom { background-position: -240px -20px; }
+.emoji-boot { background-position: -240px -40px; }
+.emoji-bouquet { background-position: -240px -60px; }
+.emoji-bow { background-position: -240px -80px; }
+.emoji-bow_and_arrow { background-position: -240px -100px; }
+.emoji-bow_tone1 { background-position: -240px -120px; }
+.emoji-bow_tone2 { background-position: -240px -140px; }
+.emoji-bow_tone3 { background-position: -240px -160px; }
+.emoji-bow_tone4 { background-position: -240px -180px; }
+.emoji-bow_tone5 { background-position: -240px -200px; }
+.emoji-bowling { background-position: -240px -220px; }
+.emoji-boxing_glove { background-position: 0 -240px; }
+.emoji-boy { background-position: -20px -240px; }
+.emoji-boy_tone1 { background-position: -40px -240px; }
+.emoji-boy_tone2 { background-position: -60px -240px; }
+.emoji-boy_tone3 { background-position: -80px -240px; }
+.emoji-boy_tone4 { background-position: -100px -240px; }
+.emoji-boy_tone5 { background-position: -120px -240px; }
+.emoji-bread { background-position: -140px -240px; }
+.emoji-bride_with_veil { background-position: -160px -240px; }
+.emoji-bride_with_veil_tone1 { background-position: -180px -240px; }
+.emoji-bride_with_veil_tone2 { background-position: -200px -240px; }
+.emoji-bride_with_veil_tone3 { background-position: -220px -240px; }
+.emoji-bride_with_veil_tone4 { background-position: -240px -240px; }
+.emoji-bride_with_veil_tone5 { background-position: -260px 0; }
+.emoji-bridge_at_night { background-position: -260px -20px; }
+.emoji-briefcase { background-position: -260px -40px; }
+.emoji-broken_heart { background-position: -260px -60px; }
+.emoji-bug { background-position: -260px -80px; }
+.emoji-bulb { background-position: -260px -100px; }
+.emoji-bullettrain_front { background-position: -260px -120px; }
+.emoji-bullettrain_side { background-position: -260px -140px; }
+.emoji-burrito { background-position: -260px -160px; }
+.emoji-bus { background-position: -260px -180px; }
+.emoji-busstop { background-position: -260px -200px; }
+.emoji-bust_in_silhouette { background-position: -260px -220px; }
+.emoji-busts_in_silhouette { background-position: -260px -240px; }
+.emoji-butterfly { background-position: 0 -260px; }
+.emoji-cactus { background-position: -20px -260px; }
+.emoji-cake { background-position: -40px -260px; }
+.emoji-calendar { background-position: -60px -260px; }
+.emoji-calendar_spiral { background-position: -80px -260px; }
+.emoji-call_me { background-position: -100px -260px; }
+.emoji-call_me_tone1 { background-position: -120px -260px; }
+.emoji-call_me_tone2 { background-position: -140px -260px; }
+.emoji-call_me_tone3 { background-position: -160px -260px; }
+.emoji-call_me_tone4 { background-position: -180px -260px; }
+.emoji-call_me_tone5 { background-position: -200px -260px; }
+.emoji-calling { background-position: -220px -260px; }
+.emoji-camel { background-position: -240px -260px; }
+.emoji-camera { background-position: -260px -260px; }
+.emoji-camera_with_flash { background-position: -280px 0; }
+.emoji-camping { background-position: -280px -20px; }
+.emoji-cancer { background-position: -280px -40px; }
+.emoji-candle { background-position: -280px -60px; }
+.emoji-candy { background-position: -280px -80px; }
+.emoji-canoe { background-position: -280px -100px; }
+.emoji-capital_abcd { background-position: -280px -120px; }
+.emoji-capricorn { background-position: -280px -140px; }
+.emoji-card_box { background-position: -280px -160px; }
+.emoji-card_index { background-position: -280px -180px; }
+.emoji-carousel_horse { background-position: -280px -200px; }
+.emoji-carrot { background-position: -280px -220px; }
+.emoji-cartwheel { background-position: -280px -240px; }
+.emoji-cartwheel_tone1 { background-position: -280px -260px; }
+.emoji-cartwheel_tone2 { background-position: 0 -280px; }
+.emoji-cartwheel_tone3 { background-position: -20px -280px; }
+.emoji-cartwheel_tone4 { background-position: -40px -280px; }
+.emoji-cartwheel_tone5 { background-position: -60px -280px; }
+.emoji-cat { background-position: -80px -280px; }
+.emoji-cat2 { background-position: -100px -280px; }
+.emoji-cd { background-position: -120px -280px; }
+.emoji-chains { background-position: -140px -280px; }
+.emoji-champagne { background-position: -160px -280px; }
+.emoji-champagne_glass { background-position: -180px -280px; }
+.emoji-chart { background-position: -200px -280px; }
+.emoji-chart_with_downwards_trend { background-position: -220px -280px; }
+.emoji-chart_with_upwards_trend { background-position: -240px -280px; }
+.emoji-checkered_flag { background-position: -260px -280px; }
+.emoji-cheese { background-position: -280px -280px; }
+.emoji-cherries { background-position: -300px 0; }
+.emoji-cherry_blossom { background-position: -300px -20px; }
+.emoji-chestnut { background-position: -300px -40px; }
+.emoji-chicken { background-position: -300px -60px; }
+.emoji-children_crossing { background-position: -300px -80px; }
+.emoji-chipmunk { background-position: -300px -100px; }
+.emoji-chocolate_bar { background-position: -300px -120px; }
+.emoji-christmas_tree { background-position: -300px -140px; }
+.emoji-church { background-position: -300px -160px; }
+.emoji-cinema { background-position: -300px -180px; }
+.emoji-circus_tent { background-position: -300px -200px; }
+.emoji-city_dusk { background-position: -300px -220px; }
+.emoji-city_sunset { background-position: -300px -240px; }
+.emoji-cityscape { background-position: -300px -260px; }
+.emoji-cl { background-position: -300px -280px; }
+.emoji-clap { background-position: 0 -300px; }
+.emoji-clap_tone1 { background-position: -20px -300px; }
+.emoji-clap_tone2 { background-position: -40px -300px; }
+.emoji-clap_tone3 { background-position: -60px -300px; }
+.emoji-clap_tone4 { background-position: -80px -300px; }
+.emoji-clap_tone5 { background-position: -100px -300px; }
+.emoji-clapper { background-position: -120px -300px; }
+.emoji-classical_building { background-position: -140px -300px; }
+.emoji-clipboard { background-position: -160px -300px; }
+.emoji-clock { background-position: -180px -300px; }
+.emoji-clock1 { background-position: -200px -300px; }
+.emoji-clock10 { background-position: -220px -300px; }
+.emoji-clock1030 { background-position: -240px -300px; }
+.emoji-clock11 { background-position: -260px -300px; }
+.emoji-clock1130 { background-position: -280px -300px; }
+.emoji-clock12 { background-position: -300px -300px; }
+.emoji-clock1230 { background-position: -320px 0; }
+.emoji-clock130 { background-position: -320px -20px; }
+.emoji-clock2 { background-position: -320px -40px; }
+.emoji-clock230 { background-position: -320px -60px; }
+.emoji-clock3 { background-position: -320px -80px; }
+.emoji-clock330 { background-position: -320px -100px; }
+.emoji-clock4 { background-position: -320px -120px; }
+.emoji-clock430 { background-position: -320px -140px; }
+.emoji-clock5 { background-position: -320px -160px; }
+.emoji-clock530 { background-position: -320px -180px; }
+.emoji-clock6 { background-position: -320px -200px; }
+.emoji-clock630 { background-position: -320px -220px; }
+.emoji-clock7 { background-position: -320px -240px; }
+.emoji-clock730 { background-position: -320px -260px; }
+.emoji-clock8 { background-position: -320px -280px; }
+.emoji-clock830 { background-position: -320px -300px; }
+.emoji-clock9 { background-position: 0 -320px; }
+.emoji-clock930 { background-position: -20px -320px; }
+.emoji-closed_book { background-position: -40px -320px; }
+.emoji-closed_lock_with_key { background-position: -60px -320px; }
+.emoji-closed_umbrella { background-position: -80px -320px; }
+.emoji-cloud { background-position: -100px -320px; }
+.emoji-cloud_lightning { background-position: -120px -320px; }
+.emoji-cloud_rain { background-position: -140px -320px; }
+.emoji-cloud_snow { background-position: -160px -320px; }
+.emoji-cloud_tornado { background-position: -180px -320px; }
+.emoji-clown { background-position: -200px -320px; }
+.emoji-clubs { background-position: -220px -320px; }
+.emoji-cocktail { background-position: -240px -320px; }
+.emoji-coffee { background-position: -260px -320px; }
+.emoji-coffin { background-position: -280px -320px; }
+.emoji-cold_sweat { background-position: -300px -320px; }
+.emoji-comet { background-position: -320px -320px; }
+.emoji-compression { background-position: -340px 0; }
+.emoji-computer { background-position: -340px -20px; }
+.emoji-confetti_ball { background-position: -340px -40px; }
+.emoji-confounded { background-position: -340px -60px; }
+.emoji-confused { background-position: -340px -80px; }
+.emoji-congratulations { background-position: -340px -100px; }
+.emoji-construction { background-position: -340px -120px; }
+.emoji-construction_site { background-position: -340px -140px; }
+.emoji-construction_worker { background-position: -340px -160px; }
+.emoji-construction_worker_tone1 { background-position: -340px -180px; }
+.emoji-construction_worker_tone2 { background-position: -340px -200px; }
+.emoji-construction_worker_tone3 { background-position: -340px -220px; }
+.emoji-construction_worker_tone4 { background-position: -340px -240px; }
+.emoji-construction_worker_tone5 { background-position: -340px -260px; }
+.emoji-control_knobs { background-position: -340px -280px; }
+.emoji-convenience_store { background-position: -340px -300px; }
+.emoji-cookie { background-position: -340px -320px; }
+.emoji-cooking { background-position: 0 -340px; }
+.emoji-cool { background-position: -20px -340px; }
+.emoji-cop { background-position: -40px -340px; }
+.emoji-cop_tone1 { background-position: -60px -340px; }
+.emoji-cop_tone2 { background-position: -80px -340px; }
+.emoji-cop_tone3 { background-position: -100px -340px; }
+.emoji-cop_tone4 { background-position: -120px -340px; }
+.emoji-cop_tone5 { background-position: -140px -340px; }
+.emoji-copyright { background-position: -160px -340px; }
+.emoji-corn { background-position: -180px -340px; }
+.emoji-couch { background-position: -200px -340px; }
+.emoji-couple { background-position: -220px -340px; }
+.emoji-couple_mm { background-position: -240px -340px; }
+.emoji-couple_with_heart { background-position: -260px -340px; }
+.emoji-couple_ww { background-position: -280px -340px; }
+.emoji-couplekiss { background-position: -300px -340px; }
+.emoji-cow { background-position: -320px -340px; }
+.emoji-cow2 { background-position: -340px -340px; }
+.emoji-cowboy { background-position: -360px 0; }
+.emoji-crab { background-position: -360px -20px; }
+.emoji-crayon { background-position: -360px -40px; }
+.emoji-credit_card { background-position: -360px -60px; }
+.emoji-crescent_moon { background-position: -360px -80px; }
+.emoji-cricket { background-position: -360px -100px; }
+.emoji-crocodile { background-position: -360px -120px; }
+.emoji-croissant { background-position: -360px -140px; }
+.emoji-cross { background-position: -360px -160px; }
+.emoji-crossed_flags { background-position: -360px -180px; }
+.emoji-crossed_swords { background-position: -360px -200px; }
+.emoji-crown { background-position: -360px -220px; }
+.emoji-cruise_ship { background-position: -360px -240px; }
+.emoji-cry { background-position: -360px -260px; }
+.emoji-crying_cat_face { background-position: -360px -280px; }
+.emoji-crystal_ball { background-position: -360px -300px; }
+.emoji-cucumber { background-position: -360px -320px; }
+.emoji-cupid { background-position: -360px -340px; }
+.emoji-curly_loop { background-position: 0 -360px; }
+.emoji-currency_exchange { background-position: -20px -360px; }
+.emoji-curry { background-position: -40px -360px; }
+.emoji-custard { background-position: -60px -360px; }
+.emoji-customs { background-position: -80px -360px; }
+.emoji-cyclone { background-position: -100px -360px; }
+.emoji-dagger { background-position: -120px -360px; }
+.emoji-dancer { background-position: -140px -360px; }
+.emoji-dancer_tone1 { background-position: -160px -360px; }
+.emoji-dancer_tone2 { background-position: -180px -360px; }
+.emoji-dancer_tone3 { background-position: -200px -360px; }
+.emoji-dancer_tone4 { background-position: -220px -360px; }
+.emoji-dancer_tone5 { background-position: -240px -360px; }
+.emoji-dancers { background-position: -260px -360px; }
+.emoji-dango { background-position: -280px -360px; }
+.emoji-dark_sunglasses { background-position: -300px -360px; }
+.emoji-dart { background-position: -320px -360px; }
+.emoji-dash { background-position: -340px -360px; }
+.emoji-date { background-position: -360px -360px; }
+.emoji-deciduous_tree { background-position: -380px 0; }
+.emoji-deer { background-position: -380px -20px; }
+.emoji-department_store { background-position: -380px -40px; }
+.emoji-desert { background-position: -380px -60px; }
+.emoji-desktop { background-position: -380px -80px; }
+.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; }
+.emoji-diamonds { background-position: -380px -120px; }
+.emoji-disappointed { background-position: -380px -140px; }
+.emoji-disappointed_relieved { background-position: -380px -160px; }
+.emoji-dividers { background-position: -380px -180px; }
+.emoji-dizzy { background-position: -380px -200px; }
+.emoji-dizzy_face { background-position: -380px -220px; }
+.emoji-do_not_litter { background-position: -380px -240px; }
+.emoji-dog { background-position: -380px -260px; }
+.emoji-dog2 { background-position: -380px -280px; }
+.emoji-dollar { background-position: -380px -300px; }
+.emoji-dolls { background-position: -380px -320px; }
+.emoji-dolphin { background-position: -380px -340px; }
+.emoji-door { background-position: -380px -360px; }
+.emoji-doughnut { background-position: 0 -380px; }
+.emoji-dove { background-position: -20px -380px; }
+.emoji-dragon { background-position: -40px -380px; }
+.emoji-dragon_face { background-position: -60px -380px; }
+.emoji-dress { background-position: -80px -380px; }
+.emoji-dromedary_camel { background-position: -100px -380px; }
+.emoji-drooling_face { background-position: -120px -380px; }
+.emoji-droplet { background-position: -140px -380px; }
+.emoji-drum { background-position: -160px -380px; }
+.emoji-duck { background-position: -180px -380px; }
+.emoji-dvd { background-position: -200px -380px; }
+.emoji-e-mail { background-position: -220px -380px; }
+.emoji-eagle { background-position: -240px -380px; }
+.emoji-ear { background-position: -260px -380px; }
+.emoji-ear_of_rice { background-position: -280px -380px; }
+.emoji-ear_tone1 { background-position: -300px -380px; }
+.emoji-ear_tone2 { background-position: -320px -380px; }
+.emoji-ear_tone3 { background-position: -340px -380px; }
+.emoji-ear_tone4 { background-position: -360px -380px; }
+.emoji-ear_tone5 { background-position: -380px -380px; }
+.emoji-earth_africa { background-position: -400px 0; }
+.emoji-earth_americas { background-position: -400px -20px; }
+.emoji-earth_asia { background-position: -400px -40px; }
+.emoji-egg { background-position: -400px -60px; }
+.emoji-eggplant { background-position: -400px -80px; }
+.emoji-eight { background-position: -400px -100px; }
+.emoji-eight_pointed_black_star { background-position: -400px -120px; }
+.emoji-eight_spoked_asterisk { background-position: -400px -140px; }
+.emoji-eject { background-position: -400px -160px; }
+.emoji-electric_plug { background-position: -400px -180px; }
+.emoji-elephant { background-position: -400px -200px; }
+.emoji-end { background-position: -400px -220px; }
+.emoji-envelope { background-position: -400px -240px; }
+.emoji-envelope_with_arrow { background-position: -400px -260px; }
+.emoji-euro { background-position: -400px -280px; }
+.emoji-european_castle { background-position: -400px -300px; }
+.emoji-european_post_office { background-position: -400px -320px; }
+.emoji-evergreen_tree { background-position: -400px -340px; }
+.emoji-exclamation { background-position: -400px -360px; }
+.emoji-expressionless { background-position: -400px -380px; }
+.emoji-eye { background-position: 0 -400px; }
+.emoji-eye_in_speech_bubble { background-position: -20px -400px; }
+.emoji-eyeglasses { background-position: -40px -400px; }
+.emoji-eyes { background-position: -60px -400px; }
+.emoji-face_palm { background-position: -80px -400px; }
+.emoji-face_palm_tone1 { background-position: -100px -400px; }
+.emoji-face_palm_tone2 { background-position: -120px -400px; }
+.emoji-face_palm_tone3 { background-position: -140px -400px; }
+.emoji-face_palm_tone4 { background-position: -160px -400px; }
+.emoji-face_palm_tone5 { background-position: -180px -400px; }
+.emoji-factory { background-position: -200px -400px; }
+.emoji-fallen_leaf { background-position: -220px -400px; }
+.emoji-family { background-position: -240px -400px; }
+.emoji-family_mmb { background-position: -260px -400px; }
+.emoji-family_mmbb { background-position: -280px -400px; }
+.emoji-family_mmg { background-position: -300px -400px; }
+.emoji-family_mmgb { background-position: -320px -400px; }
+.emoji-family_mmgg { background-position: -340px -400px; }
+.emoji-family_mwbb { background-position: -360px -400px; }
+.emoji-family_mwg { background-position: -380px -400px; }
+.emoji-family_mwgb { background-position: -400px -400px; }
+.emoji-family_mwgg { background-position: -420px 0; }
+.emoji-family_wwb { background-position: -420px -20px; }
+.emoji-family_wwbb { background-position: -420px -40px; }
+.emoji-family_wwg { background-position: -420px -60px; }
+.emoji-family_wwgb { background-position: -420px -80px; }
+.emoji-family_wwgg { background-position: -420px -100px; }
+.emoji-fast_forward { background-position: -420px -120px; }
+.emoji-fax { background-position: -420px -140px; }
+.emoji-fearful { background-position: -420px -160px; }
+.emoji-feet { background-position: -420px -180px; }
+.emoji-fencer { background-position: -420px -200px; }
+.emoji-ferris_wheel { background-position: -420px -220px; }
+.emoji-ferry { background-position: -420px -240px; }
+.emoji-field_hockey { background-position: -420px -260px; }
+.emoji-file_cabinet { background-position: -420px -280px; }
+.emoji-file_folder { background-position: -420px -300px; }
+.emoji-film_frames { background-position: -420px -320px; }
+.emoji-fingers_crossed { background-position: -420px -340px; }
+.emoji-fingers_crossed_tone1 { background-position: -420px -360px; }
+.emoji-fingers_crossed_tone2 { background-position: -420px -380px; }
+.emoji-fingers_crossed_tone3 { background-position: -420px -400px; }
+.emoji-fingers_crossed_tone4 { background-position: 0 -420px; }
+.emoji-fingers_crossed_tone5 { background-position: -20px -420px; }
+.emoji-fire { background-position: -40px -420px; }
+.emoji-fire_engine { background-position: -60px -420px; }
+.emoji-fireworks { background-position: -80px -420px; }
+.emoji-first_place { background-position: -100px -420px; }
+.emoji-first_quarter_moon { background-position: -120px -420px; }
+.emoji-first_quarter_moon_with_face { background-position: -140px -420px; }
+.emoji-fish { background-position: -160px -420px; }
+.emoji-fish_cake { background-position: -180px -420px; }
+.emoji-fishing_pole_and_fish { background-position: -200px -420px; }
+.emoji-fist { background-position: -220px -420px; }
+.emoji-fist_tone1 { background-position: -240px -420px; }
+.emoji-fist_tone2 { background-position: -260px -420px; }
+.emoji-fist_tone3 { background-position: -280px -420px; }
+.emoji-fist_tone4 { background-position: -300px -420px; }
+.emoji-fist_tone5 { background-position: -320px -420px; }
+.emoji-five { background-position: -340px -420px; }
+.emoji-flag_ac { background-position: -360px -420px; }
+.emoji-flag_ad { background-position: -380px -420px; }
+.emoji-flag_ae { background-position: -400px -420px; }
+.emoji-flag_af { background-position: -420px -420px; }
+.emoji-flag_ag { background-position: -440px 0; }
+.emoji-flag_ai { background-position: -440px -20px; }
+.emoji-flag_al { background-position: -440px -40px; }
+.emoji-flag_am { background-position: -440px -60px; }
+.emoji-flag_ao { background-position: -440px -80px; }
+.emoji-flag_aq { background-position: -440px -100px; }
+.emoji-flag_ar { background-position: -440px -120px; }
+.emoji-flag_as { background-position: -440px -140px; }
+.emoji-flag_at { background-position: -440px -160px; }
+.emoji-flag_au { background-position: -440px -180px; }
+.emoji-flag_aw { background-position: -440px -200px; }
+.emoji-flag_ax { background-position: -440px -220px; }
+.emoji-flag_az { background-position: -440px -240px; }
+.emoji-flag_ba { background-position: -440px -260px; }
+.emoji-flag_bb { background-position: -440px -280px; }
+.emoji-flag_bd { background-position: -440px -300px; }
+.emoji-flag_be { background-position: -440px -320px; }
+.emoji-flag_bf { background-position: -440px -340px; }
+.emoji-flag_bg { background-position: -440px -360px; }
+.emoji-flag_bh { background-position: -440px -380px; }
+.emoji-flag_bi { background-position: -440px -400px; }
+.emoji-flag_bj { background-position: -440px -420px; }
+.emoji-flag_bl { background-position: 0 -440px; }
+.emoji-flag_black { background-position: -20px -440px; }
+.emoji-flag_bm { background-position: -40px -440px; }
+.emoji-flag_bn { background-position: -60px -440px; }
+.emoji-flag_bo { background-position: -80px -440px; }
+.emoji-flag_bq { background-position: -100px -440px; }
+.emoji-flag_br { background-position: -120px -440px; }
+.emoji-flag_bs { background-position: -140px -440px; }
+.emoji-flag_bt { background-position: -160px -440px; }
+.emoji-flag_bv { background-position: -180px -440px; }
+.emoji-flag_bw { background-position: -200px -440px; }
+.emoji-flag_by { background-position: -220px -440px; }
+.emoji-flag_bz { background-position: -240px -440px; }
+.emoji-flag_ca { background-position: -260px -440px; }
+.emoji-flag_cc { background-position: -280px -440px; }
+.emoji-flag_cd { background-position: -300px -440px; }
+.emoji-flag_cf { background-position: -320px -440px; }
+.emoji-flag_cg { background-position: -340px -440px; }
+.emoji-flag_ch { background-position: -360px -440px; }
+.emoji-flag_ci { background-position: -380px -440px; }
+.emoji-flag_ck { background-position: -400px -440px; }
+.emoji-flag_cl { background-position: -420px -440px; }
+.emoji-flag_cm { background-position: -440px -440px; }
+.emoji-flag_cn { background-position: -460px 0; }
+.emoji-flag_co { background-position: -460px -20px; }
+.emoji-flag_cp { background-position: -460px -40px; }
+.emoji-flag_cr { background-position: -460px -60px; }
+.emoji-flag_cu { background-position: -460px -80px; }
+.emoji-flag_cv { background-position: -460px -100px; }
+.emoji-flag_cw { background-position: -460px -120px; }
+.emoji-flag_cx { background-position: -460px -140px; }
+.emoji-flag_cy { background-position: -460px -160px; }
+.emoji-flag_cz { background-position: -460px -180px; }
+.emoji-flag_de { background-position: -460px -200px; }
+.emoji-flag_dg { background-position: -460px -220px; }
+.emoji-flag_dj { background-position: -460px -240px; }
+.emoji-flag_dk { background-position: -460px -260px; }
+.emoji-flag_dm { background-position: -460px -280px; }
+.emoji-flag_do { background-position: -460px -300px; }
+.emoji-flag_dz { background-position: -460px -320px; }
+.emoji-flag_ea { background-position: -460px -340px; }
+.emoji-flag_ec { background-position: -460px -360px; }
+.emoji-flag_ee { background-position: -460px -380px; }
+.emoji-flag_eg { background-position: -460px -400px; }
+.emoji-flag_eh { background-position: -460px -420px; }
+.emoji-flag_er { background-position: -460px -440px; }
+.emoji-flag_es { background-position: 0 -460px; }
+.emoji-flag_et { background-position: -20px -460px; }
+.emoji-flag_eu { background-position: -40px -460px; }
+.emoji-flag_fi { background-position: -60px -460px; }
+.emoji-flag_fj { background-position: -80px -460px; }
+.emoji-flag_fk { background-position: -100px -460px; }
+.emoji-flag_fm { background-position: -120px -460px; }
+.emoji-flag_fo { background-position: -140px -460px; }
+.emoji-flag_fr { background-position: -160px -460px; }
+.emoji-flag_ga { background-position: -180px -460px; }
+.emoji-flag_gb { background-position: -200px -460px; }
+.emoji-flag_gd { background-position: -220px -460px; }
+.emoji-flag_ge { background-position: -240px -460px; }
+.emoji-flag_gf { background-position: -260px -460px; }
+.emoji-flag_gg { background-position: -280px -460px; }
+.emoji-flag_gh { background-position: -300px -460px; }
+.emoji-flag_gi { background-position: -320px -460px; }
+.emoji-flag_gl { background-position: -340px -460px; }
+.emoji-flag_gm { background-position: -360px -460px; }
+.emoji-flag_gn { background-position: -380px -460px; }
+.emoji-flag_gp { background-position: -400px -460px; }
+.emoji-flag_gq { background-position: -420px -460px; }
+.emoji-flag_gr { background-position: -440px -460px; }
+.emoji-flag_gs { background-position: -460px -460px; }
+.emoji-flag_gt { background-position: -480px 0; }
+.emoji-flag_gu { background-position: -480px -20px; }
+.emoji-flag_gw { background-position: -480px -40px; }
+.emoji-flag_gy { background-position: -480px -60px; }
+.emoji-flag_hk { background-position: -480px -80px; }
+.emoji-flag_hm { background-position: -480px -100px; }
+.emoji-flag_hn { background-position: -480px -120px; }
+.emoji-flag_hr { background-position: -480px -140px; }
+.emoji-flag_ht { background-position: -480px -160px; }
+.emoji-flag_hu { background-position: -480px -180px; }
+.emoji-flag_ic { background-position: -480px -200px; }
+.emoji-flag_id { background-position: -480px -220px; }
+.emoji-flag_ie { background-position: -480px -240px; }
+.emoji-flag_il { background-position: -480px -260px; }
+.emoji-flag_im { background-position: -480px -280px; }
+.emoji-flag_in { background-position: -480px -300px; }
+.emoji-flag_io { background-position: -480px -320px; }
+.emoji-flag_iq { background-position: -480px -340px; }
+.emoji-flag_ir { background-position: -480px -360px; }
+.emoji-flag_is { background-position: -480px -380px; }
+.emoji-flag_it { background-position: -480px -400px; }
+.emoji-flag_je { background-position: -480px -420px; }
+.emoji-flag_jm { background-position: -480px -440px; }
+.emoji-flag_jo { background-position: -480px -460px; }
+.emoji-flag_jp { background-position: 0 -480px; }
+.emoji-flag_ke { background-position: -20px -480px; }
+.emoji-flag_kg { background-position: -40px -480px; }
+.emoji-flag_kh { background-position: -60px -480px; }
+.emoji-flag_ki { background-position: -80px -480px; }
+.emoji-flag_km { background-position: -100px -480px; }
+.emoji-flag_kn { background-position: -120px -480px; }
+.emoji-flag_kp { background-position: -140px -480px; }
+.emoji-flag_kr { background-position: -160px -480px; }
+.emoji-flag_kw { background-position: -180px -480px; }
+.emoji-flag_ky { background-position: -200px -480px; }
+.emoji-flag_kz { background-position: -220px -480px; }
+.emoji-flag_la { background-position: -240px -480px; }
+.emoji-flag_lb { background-position: -260px -480px; }
+.emoji-flag_lc { background-position: -280px -480px; }
+.emoji-flag_li { background-position: -300px -480px; }
+.emoji-flag_lk { background-position: -320px -480px; }
+.emoji-flag_lr { background-position: -340px -480px; }
+.emoji-flag_ls { background-position: -360px -480px; }
+.emoji-flag_lt { background-position: -380px -480px; }
+.emoji-flag_lu { background-position: -400px -480px; }
+.emoji-flag_lv { background-position: -420px -480px; }
+.emoji-flag_ly { background-position: -440px -480px; }
+.emoji-flag_ma { background-position: -460px -480px; }
+.emoji-flag_mc { background-position: -480px -480px; }
+.emoji-flag_md { background-position: -500px 0; }
+.emoji-flag_me { background-position: -500px -20px; }
+.emoji-flag_mf { background-position: -500px -40px; }
+.emoji-flag_mg { background-position: -500px -60px; }
+.emoji-flag_mh { background-position: -500px -80px; }
+.emoji-flag_mk { background-position: -500px -100px; }
+.emoji-flag_ml { background-position: -500px -120px; }
+.emoji-flag_mm { background-position: -500px -140px; }
+.emoji-flag_mn { background-position: -500px -160px; }
+.emoji-flag_mo { background-position: -500px -180px; }
+.emoji-flag_mp { background-position: -500px -200px; }
+.emoji-flag_mq { background-position: -500px -220px; }
+.emoji-flag_mr { background-position: -500px -240px; }
+.emoji-flag_ms { background-position: -500px -260px; }
+.emoji-flag_mt { background-position: -500px -280px; }
+.emoji-flag_mu { background-position: -500px -300px; }
+.emoji-flag_mv { background-position: -500px -320px; }
+.emoji-flag_mw { background-position: -500px -340px; }
+.emoji-flag_mx { background-position: -500px -360px; }
+.emoji-flag_my { background-position: -500px -380px; }
+.emoji-flag_mz { background-position: -500px -400px; }
+.emoji-flag_na { background-position: -500px -420px; }
+.emoji-flag_nc { background-position: -500px -440px; }
+.emoji-flag_ne { background-position: -500px -460px; }
+.emoji-flag_nf { background-position: -500px -480px; }
+.emoji-flag_ng { background-position: 0 -500px; }
+.emoji-flag_ni { background-position: -20px -500px; }
+.emoji-flag_nl { background-position: -40px -500px; }
+.emoji-flag_no { background-position: -60px -500px; }
+.emoji-flag_np { background-position: -80px -500px; }
+.emoji-flag_nr { background-position: -100px -500px; }
+.emoji-flag_nu { background-position: -120px -500px; }
+.emoji-flag_nz { background-position: -140px -500px; }
+.emoji-flag_om { background-position: -160px -500px; }
+.emoji-flag_pa { background-position: -180px -500px; }
+.emoji-flag_pe { background-position: -200px -500px; }
+.emoji-flag_pf { background-position: -220px -500px; }
+.emoji-flag_pg { background-position: -240px -500px; }
+.emoji-flag_ph { background-position: -260px -500px; }
+.emoji-flag_pk { background-position: -280px -500px; }
+.emoji-flag_pl { background-position: -300px -500px; }
+.emoji-flag_pm { background-position: -320px -500px; }
+.emoji-flag_pn { background-position: -340px -500px; }
+.emoji-flag_pr { background-position: -360px -500px; }
+.emoji-flag_ps { background-position: -380px -500px; }
+.emoji-flag_pt { background-position: -400px -500px; }
+.emoji-flag_pw { background-position: -420px -500px; }
+.emoji-flag_py { background-position: -440px -500px; }
+.emoji-flag_qa { background-position: -460px -500px; }
+.emoji-flag_re { background-position: -480px -500px; }
+.emoji-flag_ro { background-position: -500px -500px; }
+.emoji-flag_rs { background-position: -520px 0; }
+.emoji-flag_ru { background-position: -520px -20px; }
+.emoji-flag_rw { background-position: -520px -40px; }
+.emoji-flag_sa { background-position: -520px -60px; }
+.emoji-flag_sb { background-position: -520px -80px; }
+.emoji-flag_sc { background-position: -520px -100px; }
+.emoji-flag_sd { background-position: -520px -120px; }
+.emoji-flag_se { background-position: -520px -140px; }
+.emoji-flag_sg { background-position: -520px -160px; }
+.emoji-flag_sh { background-position: -520px -180px; }
+.emoji-flag_si { background-position: -520px -200px; }
+.emoji-flag_sj { background-position: -520px -220px; }
+.emoji-flag_sk { background-position: -520px -240px; }
+.emoji-flag_sl { background-position: -520px -260px; }
+.emoji-flag_sm { background-position: -520px -280px; }
+.emoji-flag_sn { background-position: -520px -300px; }
+.emoji-flag_so { background-position: -520px -320px; }
+.emoji-flag_sr { background-position: -520px -340px; }
+.emoji-flag_ss { background-position: -520px -360px; }
+.emoji-flag_st { background-position: -520px -380px; }
+.emoji-flag_sv { background-position: -520px -400px; }
+.emoji-flag_sx { background-position: -520px -420px; }
+.emoji-flag_sy { background-position: -520px -440px; }
+.emoji-flag_sz { background-position: -520px -460px; }
+.emoji-flag_ta { background-position: -520px -480px; }
+.emoji-flag_tc { background-position: -520px -500px; }
+.emoji-flag_td { background-position: 0 -520px; }
+.emoji-flag_tf { background-position: -20px -520px; }
+.emoji-flag_tg { background-position: -40px -520px; }
+.emoji-flag_th { background-position: -60px -520px; }
+.emoji-flag_tj { background-position: -80px -520px; }
+.emoji-flag_tk { background-position: -100px -520px; }
+.emoji-flag_tl { background-position: -120px -520px; }
+.emoji-flag_tm { background-position: -140px -520px; }
+.emoji-flag_tn { background-position: -160px -520px; }
+.emoji-flag_to { background-position: -180px -520px; }
+.emoji-flag_tr { background-position: -200px -520px; }
+.emoji-flag_tt { background-position: -220px -520px; }
+.emoji-flag_tv { background-position: -240px -520px; }
+.emoji-flag_tw { background-position: -260px -520px; }
+.emoji-flag_tz { background-position: -280px -520px; }
+.emoji-flag_ua { background-position: -300px -520px; }
+.emoji-flag_ug { background-position: -320px -520px; }
+.emoji-flag_um { background-position: -340px -520px; }
+.emoji-flag_us { background-position: -360px -520px; }
+.emoji-flag_uy { background-position: -380px -520px; }
+.emoji-flag_uz { background-position: -400px -520px; }
+.emoji-flag_va { background-position: -420px -520px; }
+.emoji-flag_vc { background-position: -440px -520px; }
+.emoji-flag_ve { background-position: -460px -520px; }
+.emoji-flag_vg { background-position: -480px -520px; }
+.emoji-flag_vi { background-position: -500px -520px; }
+.emoji-flag_vn { background-position: -520px -520px; }
+.emoji-flag_vu { background-position: -540px 0; }
+.emoji-flag_wf { background-position: -540px -20px; }
+.emoji-flag_white { background-position: -540px -40px; }
+.emoji-flag_ws { background-position: -540px -60px; }
+.emoji-flag_xk { background-position: -540px -80px; }
+.emoji-flag_ye { background-position: -540px -100px; }
+.emoji-flag_yt { background-position: -540px -120px; }
+.emoji-flag_za { background-position: -540px -140px; }
+.emoji-flag_zm { background-position: -540px -160px; }
+.emoji-flag_zw { background-position: -540px -180px; }
+.emoji-flags { background-position: -540px -200px; }
+.emoji-flashlight { background-position: -540px -220px; }
+.emoji-fleur-de-lis { background-position: -540px -240px; }
+.emoji-floppy_disk { background-position: -540px -260px; }
+.emoji-flower_playing_cards { background-position: -540px -280px; }
+.emoji-flushed { background-position: -540px -300px; }
+.emoji-fog { background-position: -540px -320px; }
+.emoji-foggy { background-position: -540px -340px; }
+.emoji-football { background-position: -540px -360px; }
+.emoji-footprints { background-position: -540px -380px; }
+.emoji-fork_and_knife { background-position: -540px -400px; }
+.emoji-fork_knife_plate { background-position: -540px -420px; }
+.emoji-fountain { background-position: -540px -440px; }
+.emoji-four { background-position: -540px -460px; }
+.emoji-four_leaf_clover { background-position: -540px -480px; }
+.emoji-fox { background-position: -540px -500px; }
+.emoji-frame_photo { background-position: -540px -520px; }
+.emoji-free { background-position: 0 -540px; }
+.emoji-french_bread { background-position: -20px -540px; }
+.emoji-fried_shrimp { background-position: -40px -540px; }
+.emoji-fries { background-position: -60px -540px; }
+.emoji-frog { background-position: -80px -540px; }
+.emoji-frowning { background-position: -100px -540px; }
+.emoji-frowning2 { background-position: -120px -540px; }
+.emoji-fuelpump { background-position: -140px -540px; }
+.emoji-full_moon { background-position: -160px -540px; }
+.emoji-full_moon_with_face { background-position: -180px -540px; }
+.emoji-game_die { background-position: -200px -540px; }
+.emoji-gear { background-position: -220px -540px; }
+.emoji-gem { background-position: -240px -540px; }
+.emoji-gemini { background-position: -260px -540px; }
+.emoji-ghost { background-position: -280px -540px; }
+.emoji-gift { background-position: -300px -540px; }
+.emoji-gift_heart { background-position: -320px -540px; }
+.emoji-girl { background-position: -340px -540px; }
+.emoji-girl_tone1 { background-position: -360px -540px; }
+.emoji-girl_tone2 { background-position: -380px -540px; }
+.emoji-girl_tone3 { background-position: -400px -540px; }
+.emoji-girl_tone4 { background-position: -420px -540px; }
+.emoji-girl_tone5 { background-position: -440px -540px; }
+.emoji-globe_with_meridians { background-position: -460px -540px; }
+.emoji-goal { background-position: -480px -540px; }
+.emoji-goat { background-position: -500px -540px; }
+.emoji-golf { background-position: -520px -540px; }
+.emoji-golfer { background-position: -540px -540px; }
+.emoji-gorilla { background-position: -560px 0; }
+.emoji-grapes { background-position: -560px -20px; }
+.emoji-green_apple { background-position: -560px -40px; }
+.emoji-green_book { background-position: -560px -60px; }
+.emoji-green_heart { background-position: -560px -80px; }
+.emoji-grey_exclamation { background-position: -560px -100px; }
+.emoji-grey_question { background-position: -560px -120px; }
+.emoji-grimacing { background-position: -560px -140px; }
+.emoji-grin { background-position: -560px -160px; }
+.emoji-grinning { background-position: -560px -180px; }
+.emoji-guardsman { background-position: -560px -200px; }
+.emoji-guardsman_tone1 { background-position: -560px -220px; }
+.emoji-guardsman_tone2 { background-position: -560px -240px; }
+.emoji-guardsman_tone3 { background-position: -560px -260px; }
+.emoji-guardsman_tone4 { background-position: -560px -280px; }
+.emoji-guardsman_tone5 { background-position: -560px -300px; }
+.emoji-guitar { background-position: -560px -320px; }
+.emoji-gun { background-position: -560px -340px; }
+.emoji-haircut { background-position: -560px -360px; }
+.emoji-haircut_tone1 { background-position: -560px -380px; }
+.emoji-haircut_tone2 { background-position: -560px -400px; }
+.emoji-haircut_tone3 { background-position: -560px -420px; }
+.emoji-haircut_tone4 { background-position: -560px -440px; }
+.emoji-haircut_tone5 { background-position: -560px -460px; }
+.emoji-hamburger { background-position: -560px -480px; }
+.emoji-hammer { background-position: -560px -500px; }
+.emoji-hammer_pick { background-position: -560px -520px; }
+.emoji-hamster { background-position: -560px -540px; }
+.emoji-hand_splayed { background-position: 0 -560px; }
+.emoji-hand_splayed_tone1 { background-position: -20px -560px; }
+.emoji-hand_splayed_tone2 { background-position: -40px -560px; }
+.emoji-hand_splayed_tone3 { background-position: -60px -560px; }
+.emoji-hand_splayed_tone4 { background-position: -80px -560px; }
+.emoji-hand_splayed_tone5 { background-position: -100px -560px; }
+.emoji-handbag { background-position: -120px -560px; }
+.emoji-handball { background-position: -140px -560px; }
+.emoji-handball_tone1 { background-position: -160px -560px; }
+.emoji-handball_tone2 { background-position: -180px -560px; }
+.emoji-handball_tone3 { background-position: -200px -560px; }
+.emoji-handball_tone4 { background-position: -220px -560px; }
+.emoji-handball_tone5 { background-position: -240px -560px; }
+.emoji-handshake { background-position: -260px -560px; }
+.emoji-handshake_tone1 { background-position: -280px -560px; }
+.emoji-handshake_tone2 { background-position: -300px -560px; }
+.emoji-handshake_tone3 { background-position: -320px -560px; }
+.emoji-handshake_tone4 { background-position: -340px -560px; }
+.emoji-handshake_tone5 { background-position: -360px -560px; }
+.emoji-hash { background-position: -380px -560px; }
+.emoji-hatched_chick { background-position: -400px -560px; }
+.emoji-hatching_chick { background-position: -420px -560px; }
+.emoji-head_bandage { background-position: -440px -560px; }
+.emoji-headphones { background-position: -460px -560px; }
+.emoji-hear_no_evil { background-position: -480px -560px; }
+.emoji-heart { background-position: -500px -560px; }
+.emoji-heart_decoration { background-position: -520px -560px; }
+.emoji-heart_exclamation { background-position: -540px -560px; }
+.emoji-heart_eyes { background-position: -560px -560px; }
+.emoji-heart_eyes_cat { background-position: -580px 0; }
+.emoji-heartbeat { background-position: -580px -20px; }
+.emoji-heartpulse { background-position: -580px -40px; }
+.emoji-hearts { background-position: -580px -60px; }
+.emoji-heavy_check_mark { background-position: -580px -80px; }
+.emoji-heavy_division_sign { background-position: -580px -100px; }
+.emoji-heavy_dollar_sign { background-position: -580px -120px; }
+.emoji-heavy_minus_sign { background-position: -580px -140px; }
+.emoji-heavy_multiplication_x { background-position: -580px -160px; }
+.emoji-heavy_plus_sign { background-position: -580px -180px; }
+.emoji-helicopter { background-position: -580px -200px; }
+.emoji-helmet_with_cross { background-position: -580px -220px; }
+.emoji-herb { background-position: -580px -240px; }
+.emoji-hibiscus { background-position: -580px -260px; }
+.emoji-high_brightness { background-position: -580px -280px; }
+.emoji-high_heel { background-position: -580px -300px; }
+.emoji-hockey { background-position: -580px -320px; }
+.emoji-hole { background-position: -580px -340px; }
+.emoji-homes { background-position: -580px -360px; }
+.emoji-honey_pot { background-position: -580px -380px; }
+.emoji-horse { background-position: -580px -400px; }
+.emoji-horse_racing { background-position: -580px -420px; }
+.emoji-horse_racing_tone1 { background-position: -580px -440px; }
+.emoji-horse_racing_tone2 { background-position: -580px -460px; }
+.emoji-horse_racing_tone3 { background-position: -580px -480px; }
+.emoji-horse_racing_tone4 { background-position: -580px -500px; }
+.emoji-horse_racing_tone5 { background-position: -580px -520px; }
+.emoji-hospital { background-position: -580px -540px; }
+.emoji-hot_pepper { background-position: -580px -560px; }
+.emoji-hotdog { background-position: 0 -580px; }
+.emoji-hotel { background-position: -20px -580px; }
+.emoji-hotsprings { background-position: -40px -580px; }
+.emoji-hourglass { background-position: -60px -580px; }
+.emoji-hourglass_flowing_sand { background-position: -80px -580px; }
+.emoji-house { background-position: -100px -580px; }
+.emoji-house_abandoned { background-position: -120px -580px; }
+.emoji-house_with_garden { background-position: -140px -580px; }
+.emoji-hugging { background-position: -160px -580px; }
+.emoji-hushed { background-position: -180px -580px; }
+.emoji-ice_cream { background-position: -200px -580px; }
+.emoji-ice_skate { background-position: -220px -580px; }
+.emoji-icecream { background-position: -240px -580px; }
+.emoji-id { background-position: -260px -580px; }
+.emoji-ideograph_advantage { background-position: -280px -580px; }
+.emoji-imp { background-position: -300px -580px; }
+.emoji-inbox_tray { background-position: -320px -580px; }
+.emoji-incoming_envelope { background-position: -340px -580px; }
+.emoji-information_desk_person { background-position: -360px -580px; }
+.emoji-information_desk_person_tone1 { background-position: -380px -580px; }
+.emoji-information_desk_person_tone2 { background-position: -400px -580px; }
+.emoji-information_desk_person_tone3 { background-position: -420px -580px; }
+.emoji-information_desk_person_tone4 { background-position: -440px -580px; }
+.emoji-information_desk_person_tone5 { background-position: -460px -580px; }
+.emoji-information_source { background-position: -480px -580px; }
+.emoji-innocent { background-position: -500px -580px; }
+.emoji-interrobang { background-position: -520px -580px; }
+.emoji-iphone { background-position: -540px -580px; }
+.emoji-island { background-position: -560px -580px; }
+.emoji-izakaya_lantern { background-position: -580px -580px; }
+.emoji-jack_o_lantern { background-position: -600px 0; }
+.emoji-japan { background-position: -600px -20px; }
+.emoji-japanese_castle { background-position: -600px -40px; }
+.emoji-japanese_goblin { background-position: -600px -60px; }
+.emoji-japanese_ogre { background-position: -600px -80px; }
+.emoji-jeans { background-position: -600px -100px; }
+.emoji-joy { background-position: -600px -120px; }
+.emoji-joy_cat { background-position: -600px -140px; }
+.emoji-joystick { background-position: -600px -160px; }
+.emoji-juggling { background-position: -600px -180px; }
+.emoji-juggling_tone1 { background-position: -600px -200px; }
+.emoji-juggling_tone2 { background-position: -600px -220px; }
+.emoji-juggling_tone3 { background-position: -600px -240px; }
+.emoji-juggling_tone4 { background-position: -600px -260px; }
+.emoji-juggling_tone5 { background-position: -600px -280px; }
+.emoji-kaaba { background-position: -600px -300px; }
+.emoji-key { background-position: -600px -320px; }
+.emoji-key2 { background-position: -600px -340px; }
+.emoji-keyboard { background-position: -600px -360px; }
+.emoji-kimono { background-position: -600px -380px; }
+.emoji-kiss { background-position: -600px -400px; }
+.emoji-kiss_mm { background-position: -600px -420px; }
+.emoji-kiss_ww { background-position: -600px -440px; }
+.emoji-kissing { background-position: -600px -460px; }
+.emoji-kissing_cat { background-position: -600px -480px; }
+.emoji-kissing_closed_eyes { background-position: -600px -500px; }
+.emoji-kissing_heart { background-position: -600px -520px; }
+.emoji-kissing_smiling_eyes { background-position: -600px -540px; }
+.emoji-kiwi { background-position: -600px -560px; }
+.emoji-knife { background-position: -600px -580px; }
+.emoji-koala { background-position: 0 -600px; }
+.emoji-koko { background-position: -20px -600px; }
+.emoji-label { background-position: -40px -600px; }
+.emoji-large_blue_circle { background-position: -60px -600px; }
+.emoji-large_blue_diamond { background-position: -80px -600px; }
+.emoji-large_orange_diamond { background-position: -100px -600px; }
+.emoji-last_quarter_moon { background-position: -120px -600px; }
+.emoji-last_quarter_moon_with_face { background-position: -140px -600px; }
+.emoji-laughing { background-position: -160px -600px; }
+.emoji-leaves { background-position: -180px -600px; }
+.emoji-ledger { background-position: -200px -600px; }
+.emoji-left_facing_fist { background-position: -220px -600px; }
+.emoji-left_facing_fist_tone1 { background-position: -240px -600px; }
+.emoji-left_facing_fist_tone2 { background-position: -260px -600px; }
+.emoji-left_facing_fist_tone3 { background-position: -280px -600px; }
+.emoji-left_facing_fist_tone4 { background-position: -300px -600px; }
+.emoji-left_facing_fist_tone5 { background-position: -320px -600px; }
+.emoji-left_luggage { background-position: -340px -600px; }
+.emoji-left_right_arrow { background-position: -360px -600px; }
+.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; }
+.emoji-lemon { background-position: -400px -600px; }
+.emoji-leo { background-position: -420px -600px; }
+.emoji-leopard { background-position: -440px -600px; }
+.emoji-level_slider { background-position: -460px -600px; }
+.emoji-levitate { background-position: -480px -600px; }
+.emoji-libra { background-position: -500px -600px; }
+.emoji-lifter { background-position: -520px -600px; }
+.emoji-lifter_tone1 { background-position: -540px -600px; }
+.emoji-lifter_tone2 { background-position: -560px -600px; }
+.emoji-lifter_tone3 { background-position: -580px -600px; }
+.emoji-lifter_tone4 { background-position: -600px -600px; }
+.emoji-lifter_tone5 { background-position: -620px 0; }
+.emoji-light_rail { background-position: -620px -20px; }
+.emoji-link { background-position: -620px -40px; }
+.emoji-lion_face { background-position: -620px -60px; }
+.emoji-lips { background-position: -620px -80px; }
+.emoji-lipstick { background-position: -620px -100px; }
+.emoji-lizard { background-position: -620px -120px; }
+.emoji-lock { background-position: -620px -140px; }
+.emoji-lock_with_ink_pen { background-position: -620px -160px; }
+.emoji-lollipop { background-position: -620px -180px; }
+.emoji-loop { background-position: -620px -200px; }
+.emoji-loud_sound { background-position: -620px -220px; }
+.emoji-loudspeaker { background-position: -620px -240px; }
+.emoji-love_hotel { background-position: -620px -260px; }
+.emoji-love_letter { background-position: -620px -280px; }
+.emoji-low_brightness { background-position: -620px -300px; }
+.emoji-lying_face { background-position: -620px -320px; }
+.emoji-m { background-position: -620px -340px; }
+.emoji-mag { background-position: -620px -360px; }
+.emoji-mag_right { background-position: -620px -380px; }
+.emoji-mahjong { background-position: -620px -400px; }
+.emoji-mailbox { background-position: -620px -420px; }
+.emoji-mailbox_closed { background-position: -620px -440px; }
+.emoji-mailbox_with_mail { background-position: -620px -460px; }
+.emoji-mailbox_with_no_mail { background-position: -620px -480px; }
+.emoji-man { background-position: -620px -500px; }
+.emoji-man_dancing { background-position: -620px -520px; }
+.emoji-man_dancing_tone1 { background-position: -620px -540px; }
+.emoji-man_dancing_tone2 { background-position: -620px -560px; }
+.emoji-man_dancing_tone3 { background-position: -620px -580px; }
+.emoji-man_dancing_tone4 { background-position: -620px -600px; }
+.emoji-man_dancing_tone5 { background-position: 0 -620px; }
+.emoji-man_in_tuxedo { background-position: -20px -620px; }
+.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; }
+.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; }
+.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; }
+.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; }
+.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; }
+.emoji-man_tone1 { background-position: -140px -620px; }
+.emoji-man_tone2 { background-position: -160px -620px; }
+.emoji-man_tone3 { background-position: -180px -620px; }
+.emoji-man_tone4 { background-position: -200px -620px; }
+.emoji-man_tone5 { background-position: -220px -620px; }
+.emoji-man_with_gua_pi_mao { background-position: -240px -620px; }
+.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; }
+.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; }
+.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; }
+.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; }
+.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; }
+.emoji-man_with_turban { background-position: -360px -620px; }
+.emoji-man_with_turban_tone1 { background-position: -380px -620px; }
+.emoji-man_with_turban_tone2 { background-position: -400px -620px; }
+.emoji-man_with_turban_tone3 { background-position: -420px -620px; }
+.emoji-man_with_turban_tone4 { background-position: -440px -620px; }
+.emoji-man_with_turban_tone5 { background-position: -460px -620px; }
+.emoji-mans_shoe { background-position: -480px -620px; }
+.emoji-map { background-position: -500px -620px; }
+.emoji-maple_leaf { background-position: -520px -620px; }
+.emoji-martial_arts_uniform { background-position: -540px -620px; }
+.emoji-mask { background-position: -560px -620px; }
+.emoji-massage { background-position: -580px -620px; }
+.emoji-massage_tone1 { background-position: -600px -620px; }
+.emoji-massage_tone2 { background-position: -620px -620px; }
+.emoji-massage_tone3 { background-position: -640px 0; }
+.emoji-massage_tone4 { background-position: -640px -20px; }
+.emoji-massage_tone5 { background-position: -640px -40px; }
+.emoji-meat_on_bone { background-position: -640px -60px; }
+.emoji-medal { background-position: -640px -80px; }
+.emoji-mega { background-position: -640px -100px; }
+.emoji-melon { background-position: -640px -120px; }
+.emoji-menorah { background-position: -640px -140px; }
+.emoji-mens { background-position: -640px -160px; }
+.emoji-metal { background-position: -640px -180px; }
+.emoji-metal_tone1 { background-position: -640px -200px; }
+.emoji-metal_tone2 { background-position: -640px -220px; }
+.emoji-metal_tone3 { background-position: -640px -240px; }
+.emoji-metal_tone4 { background-position: -640px -260px; }
+.emoji-metal_tone5 { background-position: -640px -280px; }
+.emoji-metro { background-position: -640px -300px; }
+.emoji-microphone { background-position: -640px -320px; }
+.emoji-microphone2 { background-position: -640px -340px; }
+.emoji-microscope { background-position: -640px -360px; }
+.emoji-middle_finger { background-position: -640px -380px; }
+.emoji-middle_finger_tone1 { background-position: -640px -400px; }
+.emoji-middle_finger_tone2 { background-position: -640px -420px; }
+.emoji-middle_finger_tone3 { background-position: -640px -440px; }
+.emoji-middle_finger_tone4 { background-position: -640px -460px; }
+.emoji-middle_finger_tone5 { background-position: -640px -480px; }
+.emoji-military_medal { background-position: -640px -500px; }
+.emoji-milk { background-position: -640px -520px; }
+.emoji-milky_way { background-position: -640px -540px; }
+.emoji-minibus { background-position: -640px -560px; }
+.emoji-minidisc { background-position: -640px -580px; }
+.emoji-mobile_phone_off { background-position: -640px -600px; }
+.emoji-money_mouth { background-position: -640px -620px; }
+.emoji-money_with_wings { background-position: 0 -640px; }
+.emoji-moneybag { background-position: -20px -640px; }
+.emoji-monkey { background-position: -40px -640px; }
+.emoji-monkey_face { background-position: -60px -640px; }
+.emoji-monorail { background-position: -80px -640px; }
+.emoji-mortar_board { background-position: -100px -640px; }
+.emoji-mosque { background-position: -120px -640px; }
+.emoji-motor_scooter { background-position: -140px -640px; }
+.emoji-motorboat { background-position: -160px -640px; }
+.emoji-motorcycle { background-position: -180px -640px; }
+.emoji-motorway { background-position: -200px -640px; }
+.emoji-mount_fuji { background-position: -220px -640px; }
+.emoji-mountain { background-position: -240px -640px; }
+.emoji-mountain_bicyclist { background-position: -260px -640px; }
+.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; }
+.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; }
+.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; }
+.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; }
+.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; }
+.emoji-mountain_cableway { background-position: -380px -640px; }
+.emoji-mountain_railway { background-position: -400px -640px; }
+.emoji-mountain_snow { background-position: -420px -640px; }
+.emoji-mouse { background-position: -440px -640px; }
+.emoji-mouse2 { background-position: -460px -640px; }
+.emoji-mouse_three_button { background-position: -480px -640px; }
+.emoji-movie_camera { background-position: -500px -640px; }
+.emoji-moyai { background-position: -520px -640px; }
+.emoji-mrs_claus { background-position: -540px -640px; }
+.emoji-mrs_claus_tone1 { background-position: -560px -640px; }
+.emoji-mrs_claus_tone2 { background-position: -580px -640px; }
+.emoji-mrs_claus_tone3 { background-position: -600px -640px; }
+.emoji-mrs_claus_tone4 { background-position: -620px -640px; }
+.emoji-mrs_claus_tone5 { background-position: -640px -640px; }
+.emoji-muscle { background-position: -660px 0; }
+.emoji-muscle_tone1 { background-position: -660px -20px; }
+.emoji-muscle_tone2 { background-position: -660px -40px; }
+.emoji-muscle_tone3 { background-position: -660px -60px; }
+.emoji-muscle_tone4 { background-position: -660px -80px; }
+.emoji-muscle_tone5 { background-position: -660px -100px; }
+.emoji-mushroom { background-position: -660px -120px; }
+.emoji-musical_keyboard { background-position: -660px -140px; }
+.emoji-musical_note { background-position: -660px -160px; }
+.emoji-musical_score { background-position: -660px -180px; }
+.emoji-mute { background-position: -660px -200px; }
+.emoji-nail_care { background-position: -660px -220px; }
+.emoji-nail_care_tone1 { background-position: -660px -240px; }
+.emoji-nail_care_tone2 { background-position: -660px -260px; }
+.emoji-nail_care_tone3 { background-position: -660px -280px; }
+.emoji-nail_care_tone4 { background-position: -660px -300px; }
+.emoji-nail_care_tone5 { background-position: -660px -320px; }
+.emoji-name_badge { background-position: -660px -340px; }
+.emoji-nauseated_face { background-position: -660px -360px; }
+.emoji-necktie { background-position: -660px -380px; }
+.emoji-negative_squared_cross_mark { background-position: -660px -400px; }
+.emoji-nerd { background-position: -660px -420px; }
+.emoji-neutral_face { background-position: -660px -440px; }
+.emoji-new { background-position: -660px -460px; }
+.emoji-new_moon { background-position: -660px -480px; }
+.emoji-new_moon_with_face { background-position: -660px -500px; }
+.emoji-newspaper { background-position: -660px -520px; }
+.emoji-newspaper2 { background-position: -660px -540px; }
+.emoji-ng { background-position: -660px -560px; }
+.emoji-night_with_stars { background-position: -660px -580px; }
+.emoji-nine { background-position: -660px -600px; }
+.emoji-no_bell { background-position: -660px -620px; }
+.emoji-no_bicycles { background-position: -660px -640px; }
+.emoji-no_entry { background-position: 0 -660px; }
+.emoji-no_entry_sign { background-position: -20px -660px; }
+.emoji-no_good { background-position: -40px -660px; }
+.emoji-no_good_tone1 { background-position: -60px -660px; }
+.emoji-no_good_tone2 { background-position: -80px -660px; }
+.emoji-no_good_tone3 { background-position: -100px -660px; }
+.emoji-no_good_tone4 { background-position: -120px -660px; }
+.emoji-no_good_tone5 { background-position: -140px -660px; }
+.emoji-no_mobile_phones { background-position: -160px -660px; }
+.emoji-no_mouth { background-position: -180px -660px; }
+.emoji-no_pedestrians { background-position: -200px -660px; }
+.emoji-no_smoking { background-position: -220px -660px; }
+.emoji-non-potable_water { background-position: -240px -660px; }
+.emoji-nose { background-position: -260px -660px; }
+.emoji-nose_tone1 { background-position: -280px -660px; }
+.emoji-nose_tone2 { background-position: -300px -660px; }
+.emoji-nose_tone3 { background-position: -320px -660px; }
+.emoji-nose_tone4 { background-position: -340px -660px; }
+.emoji-nose_tone5 { background-position: -360px -660px; }
+.emoji-notebook { background-position: -380px -660px; }
+.emoji-notebook_with_decorative_cover { background-position: -400px -660px; }
+.emoji-notepad_spiral { background-position: -420px -660px; }
+.emoji-notes { background-position: -440px -660px; }
+.emoji-nut_and_bolt { background-position: -460px -660px; }
+.emoji-o { background-position: -480px -660px; }
+.emoji-o2 { background-position: -500px -660px; }
+.emoji-ocean { background-position: -520px -660px; }
+.emoji-octagonal_sign { background-position: -540px -660px; }
+.emoji-octopus { background-position: -560px -660px; }
+.emoji-oden { background-position: -580px -660px; }
+.emoji-office { background-position: -600px -660px; }
+.emoji-oil { background-position: -620px -660px; }
+.emoji-ok { background-position: -640px -660px; }
+.emoji-ok_hand { background-position: -660px -660px; }
+.emoji-ok_hand_tone1 { background-position: -680px 0; }
+.emoji-ok_hand_tone2 { background-position: -680px -20px; }
+.emoji-ok_hand_tone3 { background-position: -680px -40px; }
+.emoji-ok_hand_tone4 { background-position: -680px -60px; }
+.emoji-ok_hand_tone5 { background-position: -680px -80px; }
+.emoji-ok_woman { background-position: -680px -100px; }
+.emoji-ok_woman_tone1 { background-position: -680px -120px; }
+.emoji-ok_woman_tone2 { background-position: -680px -140px; }
+.emoji-ok_woman_tone3 { background-position: -680px -160px; }
+.emoji-ok_woman_tone4 { background-position: -680px -180px; }
+.emoji-ok_woman_tone5 { background-position: -680px -200px; }
+.emoji-older_man { background-position: -680px -220px; }
+.emoji-older_man_tone1 { background-position: -680px -240px; }
+.emoji-older_man_tone2 { background-position: -680px -260px; }
+.emoji-older_man_tone3 { background-position: -680px -280px; }
+.emoji-older_man_tone4 { background-position: -680px -300px; }
+.emoji-older_man_tone5 { background-position: -680px -320px; }
+.emoji-older_woman { background-position: -680px -340px; }
+.emoji-older_woman_tone1 { background-position: -680px -360px; }
+.emoji-older_woman_tone2 { background-position: -680px -380px; }
+.emoji-older_woman_tone3 { background-position: -680px -400px; }
+.emoji-older_woman_tone4 { background-position: -680px -420px; }
+.emoji-older_woman_tone5 { background-position: -680px -440px; }
+.emoji-om_symbol { background-position: -680px -460px; }
+.emoji-on { background-position: -680px -480px; }
+.emoji-oncoming_automobile { background-position: -680px -500px; }
+.emoji-oncoming_bus { background-position: -680px -520px; }
+.emoji-oncoming_police_car { background-position: -680px -540px; }
+.emoji-oncoming_taxi { background-position: -680px -560px; }
+.emoji-one { background-position: -680px -580px; }
+.emoji-open_file_folder { background-position: -680px -600px; }
+.emoji-open_hands { background-position: -680px -620px; }
+.emoji-open_hands_tone1 { background-position: -680px -640px; }
+.emoji-open_hands_tone2 { background-position: -680px -660px; }
+.emoji-open_hands_tone3 { background-position: 0 -680px; }
+.emoji-open_hands_tone4 { background-position: -20px -680px; }
+.emoji-open_hands_tone5 { background-position: -40px -680px; }
+.emoji-open_mouth { background-position: -60px -680px; }
+.emoji-ophiuchus { background-position: -80px -680px; }
+.emoji-orange_book { background-position: -100px -680px; }
+.emoji-orthodox_cross { background-position: -120px -680px; }
+.emoji-outbox_tray { background-position: -140px -680px; }
+.emoji-owl { background-position: -160px -680px; }
+.emoji-ox { background-position: -180px -680px; }
+.emoji-package { background-position: -200px -680px; }
+.emoji-page_facing_up { background-position: -220px -680px; }
+.emoji-page_with_curl { background-position: -240px -680px; }
+.emoji-pager { background-position: -260px -680px; }
+.emoji-paintbrush { background-position: -280px -680px; }
+.emoji-palm_tree { background-position: -300px -680px; }
+.emoji-pancakes { background-position: -320px -680px; }
+.emoji-panda_face { background-position: -340px -680px; }
+.emoji-paperclip { background-position: -360px -680px; }
+.emoji-paperclips { background-position: -380px -680px; }
+.emoji-park { background-position: -400px -680px; }
+.emoji-parking { background-position: -420px -680px; }
+.emoji-part_alternation_mark { background-position: -440px -680px; }
+.emoji-partly_sunny { background-position: -460px -680px; }
+.emoji-passport_control { background-position: -480px -680px; }
+.emoji-pause_button { background-position: -500px -680px; }
+.emoji-peace { background-position: -520px -680px; }
+.emoji-peach { background-position: -540px -680px; }
+.emoji-peanuts { background-position: -560px -680px; }
+.emoji-pear { background-position: -580px -680px; }
+.emoji-pen_ballpoint { background-position: -600px -680px; }
+.emoji-pen_fountain { background-position: -620px -680px; }
+.emoji-pencil { background-position: -640px -680px; }
+.emoji-pencil2 { background-position: -660px -680px; }
+.emoji-penguin { background-position: -680px -680px; }
+.emoji-pensive { background-position: -700px 0; }
+.emoji-performing_arts { background-position: -700px -20px; }
+.emoji-persevere { background-position: -700px -40px; }
+.emoji-person_frowning { background-position: -700px -60px; }
+.emoji-person_frowning_tone1 { background-position: -700px -80px; }
+.emoji-person_frowning_tone2 { background-position: -700px -100px; }
+.emoji-person_frowning_tone3 { background-position: -700px -120px; }
+.emoji-person_frowning_tone4 { background-position: -700px -140px; }
+.emoji-person_frowning_tone5 { background-position: -700px -160px; }
+.emoji-person_with_blond_hair { background-position: -700px -180px; }
+.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; }
+.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; }
+.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; }
+.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; }
+.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; }
+.emoji-person_with_pouting_face { background-position: -700px -300px; }
+.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; }
+.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; }
+.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; }
+.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; }
+.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; }
+.emoji-pick { background-position: -700px -420px; }
+.emoji-pig { background-position: -700px -440px; }
+.emoji-pig2 { background-position: -700px -460px; }
+.emoji-pig_nose { background-position: -700px -480px; }
+.emoji-pill { background-position: -700px -500px; }
+.emoji-pineapple { background-position: -700px -520px; }
+.emoji-ping_pong { background-position: -700px -540px; }
+.emoji-pisces { background-position: -700px -560px; }
+.emoji-pizza { background-position: -700px -580px; }
+.emoji-place_of_worship { background-position: -700px -600px; }
+.emoji-play_pause { background-position: -700px -620px; }
+.emoji-point_down { background-position: -700px -640px; }
+.emoji-point_down_tone1 { background-position: -700px -660px; }
+.emoji-point_down_tone2 { background-position: -700px -680px; }
+.emoji-point_down_tone3 { background-position: 0 -700px; }
+.emoji-point_down_tone4 { background-position: -20px -700px; }
+.emoji-point_down_tone5 { background-position: -40px -700px; }
+.emoji-point_left { background-position: -60px -700px; }
+.emoji-point_left_tone1 { background-position: -80px -700px; }
+.emoji-point_left_tone2 { background-position: -100px -700px; }
+.emoji-point_left_tone3 { background-position: -120px -700px; }
+.emoji-point_left_tone4 { background-position: -140px -700px; }
+.emoji-point_left_tone5 { background-position: -160px -700px; }
+.emoji-point_right { background-position: -180px -700px; }
+.emoji-point_right_tone1 { background-position: -200px -700px; }
+.emoji-point_right_tone2 { background-position: -220px -700px; }
+.emoji-point_right_tone3 { background-position: -240px -700px; }
+.emoji-point_right_tone4 { background-position: -260px -700px; }
+.emoji-point_right_tone5 { background-position: -280px -700px; }
+.emoji-point_up { background-position: -300px -700px; }
+.emoji-point_up_2 { background-position: -320px -700px; }
+.emoji-point_up_2_tone1 { background-position: -340px -700px; }
+.emoji-point_up_2_tone2 { background-position: -360px -700px; }
+.emoji-point_up_2_tone3 { background-position: -380px -700px; }
+.emoji-point_up_2_tone4 { background-position: -400px -700px; }
+.emoji-point_up_2_tone5 { background-position: -420px -700px; }
+.emoji-point_up_tone1 { background-position: -440px -700px; }
+.emoji-point_up_tone2 { background-position: -460px -700px; }
+.emoji-point_up_tone3 { background-position: -480px -700px; }
+.emoji-point_up_tone4 { background-position: -500px -700px; }
+.emoji-point_up_tone5 { background-position: -520px -700px; }
+.emoji-police_car { background-position: -540px -700px; }
+.emoji-poodle { background-position: -560px -700px; }
+.emoji-poop { background-position: -580px -700px; }
+.emoji-popcorn { background-position: -600px -700px; }
+.emoji-post_office { background-position: -620px -700px; }
+.emoji-postal_horn { background-position: -640px -700px; }
+.emoji-postbox { background-position: -660px -700px; }
+.emoji-potable_water { background-position: -680px -700px; }
+.emoji-potato { background-position: -700px -700px; }
+.emoji-pouch { background-position: -720px 0; }
+.emoji-poultry_leg { background-position: -720px -20px; }
+.emoji-pound { background-position: -720px -40px; }
+.emoji-pouting_cat { background-position: -720px -60px; }
+.emoji-pray { background-position: -720px -80px; }
+.emoji-pray_tone1 { background-position: -720px -100px; }
+.emoji-pray_tone2 { background-position: -720px -120px; }
+.emoji-pray_tone3 { background-position: -720px -140px; }
+.emoji-pray_tone4 { background-position: -720px -160px; }
+.emoji-pray_tone5 { background-position: -720px -180px; }
+.emoji-prayer_beads { background-position: -720px -200px; }
+.emoji-pregnant_woman { background-position: -720px -220px; }
+.emoji-pregnant_woman_tone1 { background-position: -720px -240px; }
+.emoji-pregnant_woman_tone2 { background-position: -720px -260px; }
+.emoji-pregnant_woman_tone3 { background-position: -720px -280px; }
+.emoji-pregnant_woman_tone4 { background-position: -720px -300px; }
+.emoji-pregnant_woman_tone5 { background-position: -720px -320px; }
+.emoji-prince { background-position: -720px -340px; }
+.emoji-prince_tone1 { background-position: -720px -360px; }
+.emoji-prince_tone2 { background-position: -720px -380px; }
+.emoji-prince_tone3 { background-position: -720px -400px; }
+.emoji-prince_tone4 { background-position: -720px -420px; }
+.emoji-prince_tone5 { background-position: -720px -440px; }
+.emoji-princess { background-position: -720px -460px; }
+.emoji-princess_tone1 { background-position: -720px -480px; }
+.emoji-princess_tone2 { background-position: -720px -500px; }
+.emoji-princess_tone3 { background-position: -720px -520px; }
+.emoji-princess_tone4 { background-position: -720px -540px; }
+.emoji-princess_tone5 { background-position: -720px -560px; }
+.emoji-printer { background-position: -720px -580px; }
+.emoji-projector { background-position: -720px -600px; }
+.emoji-punch { background-position: -720px -620px; }
+.emoji-punch_tone1 { background-position: -720px -640px; }
+.emoji-punch_tone2 { background-position: -720px -660px; }
+.emoji-punch_tone3 { background-position: -720px -680px; }
+.emoji-punch_tone4 { background-position: -720px -700px; }
+.emoji-punch_tone5 { background-position: 0 -720px; }
+.emoji-purple_heart { background-position: -20px -720px; }
+.emoji-purse { background-position: -40px -720px; }
+.emoji-pushpin { background-position: -60px -720px; }
+.emoji-put_litter_in_its_place { background-position: -80px -720px; }
+.emoji-question { background-position: -100px -720px; }
+.emoji-rabbit { background-position: -120px -720px; }
+.emoji-rabbit2 { background-position: -140px -720px; }
+.emoji-race_car { background-position: -160px -720px; }
+.emoji-racehorse { background-position: -180px -720px; }
+.emoji-radio { background-position: -200px -720px; }
+.emoji-radio_button { background-position: -220px -720px; }
+.emoji-radioactive { background-position: -240px -720px; }
+.emoji-rage { background-position: -260px -720px; }
+.emoji-railway_car { background-position: -280px -720px; }
+.emoji-railway_track { background-position: -300px -720px; }
+.emoji-rainbow { background-position: -320px -720px; }
+.emoji-raised_back_of_hand { background-position: -340px -720px; }
+.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; }
+.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; }
+.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; }
+.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; }
+.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; }
+.emoji-raised_hand { background-position: -460px -720px; }
+.emoji-raised_hand_tone1 { background-position: -480px -720px; }
+.emoji-raised_hand_tone2 { background-position: -500px -720px; }
+.emoji-raised_hand_tone3 { background-position: -520px -720px; }
+.emoji-raised_hand_tone4 { background-position: -540px -720px; }
+.emoji-raised_hand_tone5 { background-position: -560px -720px; }
+.emoji-raised_hands { background-position: -580px -720px; }
+.emoji-raised_hands_tone1 { background-position: -600px -720px; }
+.emoji-raised_hands_tone2 { background-position: -620px -720px; }
+.emoji-raised_hands_tone3 { background-position: -640px -720px; }
+.emoji-raised_hands_tone4 { background-position: -660px -720px; }
+.emoji-raised_hands_tone5 { background-position: -680px -720px; }
+.emoji-raising_hand { background-position: -700px -720px; }
+.emoji-raising_hand_tone1 { background-position: -720px -720px; }
+.emoji-raising_hand_tone2 { background-position: -740px 0; }
+.emoji-raising_hand_tone3 { background-position: -740px -20px; }
+.emoji-raising_hand_tone4 { background-position: -740px -40px; }
+.emoji-raising_hand_tone5 { background-position: -740px -60px; }
+.emoji-ram { background-position: -740px -80px; }
+.emoji-ramen { background-position: -740px -100px; }
+.emoji-rat { background-position: -740px -120px; }
+.emoji-record_button { background-position: -740px -140px; }
+.emoji-recycle { background-position: -740px -160px; }
+.emoji-red_car { background-position: -740px -180px; }
+.emoji-red_circle { background-position: -740px -200px; }
+.emoji-registered { background-position: -740px -220px; }
+.emoji-relaxed { background-position: -740px -240px; }
+.emoji-relieved { background-position: -740px -260px; }
+.emoji-reminder_ribbon { background-position: -740px -280px; }
+.emoji-repeat { background-position: -740px -300px; }
+.emoji-repeat_one { background-position: -740px -320px; }
+.emoji-restroom { background-position: -740px -340px; }
+.emoji-revolving_hearts { background-position: -740px -360px; }
+.emoji-rewind { background-position: -740px -380px; }
+.emoji-rhino { background-position: -740px -400px; }
+.emoji-ribbon { background-position: -740px -420px; }
+.emoji-rice { background-position: -740px -440px; }
+.emoji-rice_ball { background-position: -740px -460px; }
+.emoji-rice_cracker { background-position: -740px -480px; }
+.emoji-rice_scene { background-position: -740px -500px; }
+.emoji-right_facing_fist { background-position: -740px -520px; }
+.emoji-right_facing_fist_tone1 { background-position: -740px -540px; }
+.emoji-right_facing_fist_tone2 { background-position: -740px -560px; }
+.emoji-right_facing_fist_tone3 { background-position: -740px -580px; }
+.emoji-right_facing_fist_tone4 { background-position: -740px -600px; }
+.emoji-right_facing_fist_tone5 { background-position: -740px -620px; }
+.emoji-ring { background-position: -740px -640px; }
+.emoji-robot { background-position: -740px -660px; }
+.emoji-rocket { background-position: -740px -680px; }
+.emoji-rofl { background-position: -740px -700px; }
+.emoji-roller_coaster { background-position: -740px -720px; }
+.emoji-rolling_eyes { background-position: 0 -740px; }
+.emoji-rooster { background-position: -20px -740px; }
+.emoji-rose { background-position: -40px -740px; }
+.emoji-rosette { background-position: -60px -740px; }
+.emoji-rotating_light { background-position: -80px -740px; }
+.emoji-round_pushpin { background-position: -100px -740px; }
+.emoji-rowboat { background-position: -120px -740px; }
+.emoji-rowboat_tone1 { background-position: -140px -740px; }
+.emoji-rowboat_tone2 { background-position: -160px -740px; }
+.emoji-rowboat_tone3 { background-position: -180px -740px; }
+.emoji-rowboat_tone4 { background-position: -200px -740px; }
+.emoji-rowboat_tone5 { background-position: -220px -740px; }
+.emoji-rugby_football { background-position: -240px -740px; }
+.emoji-runner { background-position: -260px -740px; }
+.emoji-runner_tone1 { background-position: -280px -740px; }
+.emoji-runner_tone2 { background-position: -300px -740px; }
+.emoji-runner_tone3 { background-position: -320px -740px; }
+.emoji-runner_tone4 { background-position: -340px -740px; }
+.emoji-runner_tone5 { background-position: -360px -740px; }
+.emoji-running_shirt_with_sash { background-position: -380px -740px; }
+.emoji-sa { background-position: -400px -740px; }
+.emoji-sagittarius { background-position: -420px -740px; }
+.emoji-sailboat { background-position: -440px -740px; }
+.emoji-sake { background-position: -460px -740px; }
+.emoji-salad { background-position: -480px -740px; }
+.emoji-sandal { background-position: -500px -740px; }
+.emoji-santa { background-position: -520px -740px; }
+.emoji-santa_tone1 { background-position: -540px -740px; }
+.emoji-santa_tone2 { background-position: -560px -740px; }
+.emoji-santa_tone3 { background-position: -580px -740px; }
+.emoji-santa_tone4 { background-position: -600px -740px; }
+.emoji-santa_tone5 { background-position: -620px -740px; }
+.emoji-satellite { background-position: -640px -740px; }
+.emoji-satellite_orbital { background-position: -660px -740px; }
+.emoji-saxophone { background-position: -680px -740px; }
+.emoji-scales { background-position: -700px -740px; }
+.emoji-school { background-position: -720px -740px; }
+.emoji-school_satchel { background-position: -740px -740px; }
+.emoji-scissors { background-position: -760px 0; }
+.emoji-scooter { background-position: -760px -20px; }
+.emoji-scorpion { background-position: -760px -40px; }
+.emoji-scorpius { background-position: -760px -60px; }
+.emoji-scream { background-position: -760px -80px; }
+.emoji-scream_cat { background-position: -760px -100px; }
+.emoji-scroll { background-position: -760px -120px; }
+.emoji-seat { background-position: -760px -140px; }
+.emoji-second_place { background-position: -760px -160px; }
+.emoji-secret { background-position: -760px -180px; }
+.emoji-see_no_evil { background-position: -760px -200px; }
+.emoji-seedling { background-position: -760px -220px; }
+.emoji-selfie { background-position: -760px -240px; }
+.emoji-selfie_tone1 { background-position: -760px -260px; }
+.emoji-selfie_tone2 { background-position: -760px -280px; }
+.emoji-selfie_tone3 { background-position: -760px -300px; }
+.emoji-selfie_tone4 { background-position: -760px -320px; }
+.emoji-selfie_tone5 { background-position: -760px -340px; }
+.emoji-seven { background-position: -760px -360px; }
+.emoji-shallow_pan_of_food { background-position: -760px -380px; }
+.emoji-shamrock { background-position: -760px -400px; }
+.emoji-shark { background-position: -760px -420px; }
+.emoji-shaved_ice { background-position: -760px -440px; }
+.emoji-sheep { background-position: -760px -460px; }
+.emoji-shell { background-position: -760px -480px; }
+.emoji-shield { background-position: -760px -500px; }
+.emoji-shinto_shrine { background-position: -760px -520px; }
+.emoji-ship { background-position: -760px -540px; }
+.emoji-shirt { background-position: -760px -560px; }
+.emoji-shopping_bags { background-position: -760px -580px; }
+.emoji-shopping_cart { background-position: -760px -600px; }
+.emoji-shower { background-position: -760px -620px; }
+.emoji-shrimp { background-position: -760px -640px; }
+.emoji-shrug { background-position: -760px -660px; }
+.emoji-shrug_tone1 { background-position: -760px -680px; }
+.emoji-shrug_tone2 { background-position: -760px -700px; }
+.emoji-shrug_tone3 { background-position: -760px -720px; }
+.emoji-shrug_tone4 { background-position: -760px -740px; }
+.emoji-shrug_tone5 { background-position: 0 -760px; }
+.emoji-signal_strength { background-position: -20px -760px; }
+.emoji-six { background-position: -40px -760px; }
+.emoji-six_pointed_star { background-position: -60px -760px; }
+.emoji-ski { background-position: -80px -760px; }
+.emoji-skier { background-position: -100px -760px; }
+.emoji-skull { background-position: -120px -760px; }
+.emoji-skull_crossbones { background-position: -140px -760px; }
+.emoji-sleeping { background-position: -160px -760px; }
+.emoji-sleeping_accommodation { background-position: -180px -760px; }
+.emoji-sleepy { background-position: -200px -760px; }
+.emoji-slight_frown { background-position: -220px -760px; }
+.emoji-slight_smile { background-position: -240px -760px; }
+.emoji-slot_machine { background-position: -260px -760px; }
+.emoji-small_blue_diamond { background-position: -280px -760px; }
+.emoji-small_orange_diamond { background-position: -300px -760px; }
+.emoji-small_red_triangle { background-position: -320px -760px; }
+.emoji-small_red_triangle_down { background-position: -340px -760px; }
+.emoji-smile { background-position: -360px -760px; }
+.emoji-smile_cat { background-position: -380px -760px; }
+.emoji-smiley { background-position: -400px -760px; }
+.emoji-smiley_cat { background-position: -420px -760px; }
+.emoji-smiling_imp { background-position: -440px -760px; }
+.emoji-smirk { background-position: -460px -760px; }
+.emoji-smirk_cat { background-position: -480px -760px; }
+.emoji-smoking { background-position: -500px -760px; }
+.emoji-snail { background-position: -520px -760px; }
+.emoji-snake { background-position: -540px -760px; }
+.emoji-sneezing_face { background-position: -560px -760px; }
+.emoji-snowboarder { background-position: -580px -760px; }
+.emoji-snowflake { background-position: -600px -760px; }
+.emoji-snowman { background-position: -620px -760px; }
+.emoji-snowman2 { background-position: -640px -760px; }
+.emoji-sob { background-position: -660px -760px; }
+.emoji-soccer { background-position: -680px -760px; }
+.emoji-soon { background-position: -700px -760px; }
+.emoji-sos { background-position: -720px -760px; }
+.emoji-sound { background-position: -740px -760px; }
+.emoji-space_invader { background-position: -760px -760px; }
+.emoji-spades { background-position: -780px 0; }
+.emoji-spaghetti { background-position: -780px -20px; }
+.emoji-sparkle { background-position: -780px -40px; }
+.emoji-sparkler { background-position: -780px -60px; }
+.emoji-sparkles { background-position: -780px -80px; }
+.emoji-sparkling_heart { background-position: -780px -100px; }
+.emoji-speak_no_evil { background-position: -780px -120px; }
+.emoji-speaker { background-position: -780px -140px; }
+.emoji-speaking_head { background-position: -780px -160px; }
+.emoji-speech_balloon { background-position: -780px -180px; }
+.emoji-speedboat { background-position: -780px -200px; }
+.emoji-spider { background-position: -780px -220px; }
+.emoji-spider_web { background-position: -780px -240px; }
+.emoji-spoon { background-position: -780px -260px; }
+.emoji-spy { background-position: -780px -280px; }
+.emoji-spy_tone1 { background-position: -780px -300px; }
+.emoji-spy_tone2 { background-position: -780px -320px; }
+.emoji-spy_tone3 { background-position: -780px -340px; }
+.emoji-spy_tone4 { background-position: -780px -360px; }
+.emoji-spy_tone5 { background-position: -780px -380px; }
+.emoji-squid { background-position: -780px -400px; }
+.emoji-stadium { background-position: -780px -420px; }
+.emoji-star { background-position: -780px -440px; }
+.emoji-star2 { background-position: -780px -460px; }
+.emoji-star_and_crescent { background-position: -780px -480px; }
+.emoji-star_of_david { background-position: -780px -500px; }
+.emoji-stars { background-position: -780px -520px; }
+.emoji-station { background-position: -780px -540px; }
+.emoji-statue_of_liberty { background-position: -780px -560px; }
+.emoji-steam_locomotive { background-position: -780px -580px; }
+.emoji-stew { background-position: -780px -600px; }
+.emoji-stop_button { background-position: -780px -620px; }
+.emoji-stopwatch { background-position: -780px -640px; }
+.emoji-straight_ruler { background-position: -780px -660px; }
+.emoji-strawberry { background-position: -780px -680px; }
+.emoji-stuck_out_tongue { background-position: -780px -700px; }
+.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; }
+.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; }
+.emoji-stuffed_flatbread { background-position: -780px -760px; }
+.emoji-sun_with_face { background-position: 0 -780px; }
+.emoji-sunflower { background-position: -20px -780px; }
+.emoji-sunglasses { background-position: -40px -780px; }
+.emoji-sunny { background-position: -60px -780px; }
+.emoji-sunrise { background-position: -80px -780px; }
+.emoji-sunrise_over_mountains { background-position: -100px -780px; }
+.emoji-surfer { background-position: -120px -780px; }
+.emoji-surfer_tone1 { background-position: -140px -780px; }
+.emoji-surfer_tone2 { background-position: -160px -780px; }
+.emoji-surfer_tone3 { background-position: -180px -780px; }
+.emoji-surfer_tone4 { background-position: -200px -780px; }
+.emoji-surfer_tone5 { background-position: -220px -780px; }
+.emoji-sushi { background-position: -240px -780px; }
+.emoji-suspension_railway { background-position: -260px -780px; }
+.emoji-sweat { background-position: -280px -780px; }
+.emoji-sweat_drops { background-position: -300px -780px; }
+.emoji-sweat_smile { background-position: -320px -780px; }
+.emoji-sweet_potato { background-position: -340px -780px; }
+.emoji-swimmer { background-position: -360px -780px; }
+.emoji-swimmer_tone1 { background-position: -380px -780px; }
+.emoji-swimmer_tone2 { background-position: -400px -780px; }
+.emoji-swimmer_tone3 { background-position: -420px -780px; }
+.emoji-swimmer_tone4 { background-position: -440px -780px; }
+.emoji-swimmer_tone5 { background-position: -460px -780px; }
+.emoji-symbols { background-position: -480px -780px; }
+.emoji-synagogue { background-position: -500px -780px; }
+.emoji-syringe { background-position: -520px -780px; }
+.emoji-taco { background-position: -540px -780px; }
+.emoji-tada { background-position: -560px -780px; }
+.emoji-tanabata_tree { background-position: -580px -780px; }
+.emoji-tangerine { background-position: -600px -780px; }
+.emoji-taurus { background-position: -620px -780px; }
+.emoji-taxi { background-position: -640px -780px; }
+.emoji-tea { background-position: -660px -780px; }
+.emoji-telephone { background-position: -680px -780px; }
+.emoji-telephone_receiver { background-position: -700px -780px; }
+.emoji-telescope { background-position: -720px -780px; }
+.emoji-ten { background-position: -740px -780px; }
+.emoji-tennis { background-position: -760px -780px; }
+.emoji-tent { background-position: -780px -780px; }
+.emoji-thermometer { background-position: -800px 0; }
+.emoji-thermometer_face { background-position: -800px -20px; }
+.emoji-thinking { background-position: -800px -40px; }
+.emoji-third_place { background-position: -800px -60px; }
+.emoji-thought_balloon { background-position: -800px -80px; }
+.emoji-three { background-position: -800px -100px; }
+.emoji-thumbsdown { background-position: -800px -120px; }
+.emoji-thumbsdown_tone1 { background-position: -800px -140px; }
+.emoji-thumbsdown_tone2 { background-position: -800px -160px; }
+.emoji-thumbsdown_tone3 { background-position: -800px -180px; }
+.emoji-thumbsdown_tone4 { background-position: -800px -200px; }
+.emoji-thumbsdown_tone5 { background-position: -800px -220px; }
+.emoji-thumbsup { background-position: -800px -240px; }
+.emoji-thumbsup_tone1 { background-position: -800px -260px; }
+.emoji-thumbsup_tone2 { background-position: -800px -280px; }
+.emoji-thumbsup_tone3 { background-position: -800px -300px; }
+.emoji-thumbsup_tone4 { background-position: -800px -320px; }
+.emoji-thumbsup_tone5 { background-position: -800px -340px; }
+.emoji-thunder_cloud_rain { background-position: -800px -360px; }
+.emoji-ticket { background-position: -800px -380px; }
+.emoji-tickets { background-position: -800px -400px; }
+.emoji-tiger { background-position: -800px -420px; }
+.emoji-tiger2 { background-position: -800px -440px; }
+.emoji-timer { background-position: -800px -460px; }
+.emoji-tired_face { background-position: -800px -480px; }
+.emoji-tm { background-position: -800px -500px; }
+.emoji-toilet { background-position: -800px -520px; }
+.emoji-tokyo_tower { background-position: -800px -540px; }
+.emoji-tomato { background-position: -800px -560px; }
+.emoji-tone1 { background-position: -800px -580px; }
+.emoji-tone2 { background-position: -800px -600px; }
+.emoji-tone3 { background-position: -800px -620px; }
+.emoji-tone4 { background-position: -800px -640px; }
+.emoji-tone5 { background-position: -800px -660px; }
+.emoji-tongue { background-position: -800px -680px; }
+.emoji-tools { background-position: -800px -700px; }
+.emoji-top { background-position: -800px -720px; }
+.emoji-tophat { background-position: -800px -740px; }
+.emoji-track_next { background-position: -800px -760px; }
+.emoji-track_previous { background-position: -800px -780px; }
+.emoji-trackball { background-position: 0 -800px; }
+.emoji-tractor { background-position: -20px -800px; }
+.emoji-traffic_light { background-position: -40px -800px; }
+.emoji-train { background-position: -60px -800px; }
+.emoji-train2 { background-position: -80px -800px; }
+.emoji-tram { background-position: -100px -800px; }
+.emoji-triangular_flag_on_post { background-position: -120px -800px; }
+.emoji-triangular_ruler { background-position: -140px -800px; }
+.emoji-trident { background-position: -160px -800px; }
+.emoji-triumph { background-position: -180px -800px; }
+.emoji-trolleybus { background-position: -200px -800px; }
+.emoji-trophy { background-position: -220px -800px; }
+.emoji-tropical_drink { background-position: -240px -800px; }
+.emoji-tropical_fish { background-position: -260px -800px; }
+.emoji-truck { background-position: -280px -800px; }
+.emoji-trumpet { background-position: -300px -800px; }
+.emoji-tulip { background-position: -320px -800px; }
+.emoji-tumbler_glass { background-position: -340px -800px; }
+.emoji-turkey { background-position: -360px -800px; }
+.emoji-turtle { background-position: -380px -800px; }
+.emoji-tv { background-position: -400px -800px; }
+.emoji-twisted_rightwards_arrows { background-position: -420px -800px; }
+.emoji-two { background-position: -440px -800px; }
+.emoji-two_hearts { background-position: -460px -800px; }
+.emoji-two_men_holding_hands { background-position: -480px -800px; }
+.emoji-two_women_holding_hands { background-position: -500px -800px; }
+.emoji-u5272 { background-position: -520px -800px; }
+.emoji-u5408 { background-position: -540px -800px; }
+.emoji-u55b6 { background-position: -560px -800px; }
+.emoji-u6307 { background-position: -580px -800px; }
+.emoji-u6708 { background-position: -600px -800px; }
+.emoji-u6709 { background-position: -620px -800px; }
+.emoji-u6e80 { background-position: -640px -800px; }
+.emoji-u7121 { background-position: -660px -800px; }
+.emoji-u7533 { background-position: -680px -800px; }
+.emoji-u7981 { background-position: -700px -800px; }
+.emoji-u7a7a { background-position: -720px -800px; }
+.emoji-umbrella { background-position: -740px -800px; }
+.emoji-umbrella2 { background-position: -760px -800px; }
+.emoji-unamused { background-position: -780px -800px; }
+.emoji-underage { background-position: -800px -800px; }
+.emoji-unicorn { background-position: -820px 0; }
+.emoji-unlock { background-position: -820px -20px; }
+.emoji-up { background-position: -820px -40px; }
+.emoji-upside_down { background-position: -820px -60px; }
+.emoji-urn { background-position: -820px -80px; }
+.emoji-v { background-position: -820px -100px; }
+.emoji-v_tone1 { background-position: -820px -120px; }
+.emoji-v_tone2 { background-position: -820px -140px; }
+.emoji-v_tone3 { background-position: -820px -160px; }
+.emoji-v_tone4 { background-position: -820px -180px; }
+.emoji-v_tone5 { background-position: -820px -200px; }
+.emoji-vertical_traffic_light { background-position: -820px -220px; }
+.emoji-vhs { background-position: -820px -240px; }
+.emoji-vibration_mode { background-position: -820px -260px; }
+.emoji-video_camera { background-position: -820px -280px; }
+.emoji-video_game { background-position: -820px -300px; }
+.emoji-violin { background-position: -820px -320px; }
+.emoji-virgo { background-position: -820px -340px; }
+.emoji-volcano { background-position: -820px -360px; }
+.emoji-volleyball { background-position: -820px -380px; }
+.emoji-vs { background-position: -820px -400px; }
+.emoji-vulcan { background-position: -820px -420px; }
+.emoji-vulcan_tone1 { background-position: -820px -440px; }
+.emoji-vulcan_tone2 { background-position: -820px -460px; }
+.emoji-vulcan_tone3 { background-position: -820px -480px; }
+.emoji-vulcan_tone4 { background-position: -820px -500px; }
+.emoji-vulcan_tone5 { background-position: -820px -520px; }
+.emoji-walking { background-position: -820px -540px; }
+.emoji-walking_tone1 { background-position: -820px -560px; }
+.emoji-walking_tone2 { background-position: -820px -580px; }
+.emoji-walking_tone3 { background-position: -820px -600px; }
+.emoji-walking_tone4 { background-position: -820px -620px; }
+.emoji-walking_tone5 { background-position: -820px -640px; }
+.emoji-waning_crescent_moon { background-position: -820px -660px; }
+.emoji-waning_gibbous_moon { background-position: -820px -680px; }
+.emoji-warning { background-position: -820px -700px; }
+.emoji-wastebasket { background-position: -820px -720px; }
+.emoji-watch { background-position: -820px -740px; }
+.emoji-water_buffalo { background-position: -820px -760px; }
+.emoji-water_polo { background-position: -820px -780px; }
+.emoji-water_polo_tone1 { background-position: -820px -800px; }
+.emoji-water_polo_tone2 { background-position: 0 -820px; }
+.emoji-water_polo_tone3 { background-position: -20px -820px; }
+.emoji-water_polo_tone4 { background-position: -40px -820px; }
+.emoji-water_polo_tone5 { background-position: -60px -820px; }
+.emoji-watermelon { background-position: -80px -820px; }
+.emoji-wave { background-position: -100px -820px; }
+.emoji-wave_tone1 { background-position: -120px -820px; }
+.emoji-wave_tone2 { background-position: -140px -820px; }
+.emoji-wave_tone3 { background-position: -160px -820px; }
+.emoji-wave_tone4 { background-position: -180px -820px; }
+.emoji-wave_tone5 { background-position: -200px -820px; }
+.emoji-wavy_dash { background-position: -220px -820px; }
+.emoji-waxing_crescent_moon { background-position: -240px -820px; }
+.emoji-waxing_gibbous_moon { background-position: -260px -820px; }
+.emoji-wc { background-position: -280px -820px; }
+.emoji-weary { background-position: -300px -820px; }
+.emoji-wedding { background-position: -320px -820px; }
+.emoji-whale { background-position: -340px -820px; }
+.emoji-whale2 { background-position: -360px -820px; }
+.emoji-wheel_of_dharma { background-position: -380px -820px; }
+.emoji-wheelchair { background-position: -400px -820px; }
+.emoji-white_check_mark { background-position: -420px -820px; }
+.emoji-white_circle { background-position: -440px -820px; }
+.emoji-white_flower { background-position: -460px -820px; }
+.emoji-white_large_square { background-position: -480px -820px; }
+.emoji-white_medium_small_square { background-position: -500px -820px; }
+.emoji-white_medium_square { background-position: -520px -820px; }
+.emoji-white_small_square { background-position: -540px -820px; }
+.emoji-white_square_button { background-position: -560px -820px; }
+.emoji-white_sun_cloud { background-position: -580px -820px; }
+.emoji-white_sun_rain_cloud { background-position: -600px -820px; }
+.emoji-white_sun_small_cloud { background-position: -620px -820px; }
+.emoji-wilted_rose { background-position: -640px -820px; }
+.emoji-wind_blowing_face { background-position: -660px -820px; }
+.emoji-wind_chime { background-position: -680px -820px; }
+.emoji-wine_glass { background-position: -700px -820px; }
+.emoji-wink { background-position: -720px -820px; }
+.emoji-wolf { background-position: -740px -820px; }
+.emoji-woman { background-position: -760px -820px; }
+.emoji-woman_tone1 { background-position: -780px -820px; }
+.emoji-woman_tone2 { background-position: -800px -820px; }
+.emoji-woman_tone3 { background-position: -820px -820px; }
+.emoji-woman_tone4 { background-position: -840px 0; }
+.emoji-woman_tone5 { background-position: -840px -20px; }
+.emoji-womans_clothes { background-position: -840px -40px; }
+.emoji-womans_hat { background-position: -840px -60px; }
+.emoji-womens { background-position: -840px -80px; }
+.emoji-worried { background-position: -840px -100px; }
+.emoji-wrench { background-position: -840px -120px; }
+.emoji-wrestlers { background-position: -840px -140px; }
+.emoji-wrestlers_tone1 { background-position: -840px -160px; }
+.emoji-wrestlers_tone2 { background-position: -840px -180px; }
+.emoji-wrestlers_tone3 { background-position: -840px -200px; }
+.emoji-wrestlers_tone4 { background-position: -840px -220px; }
+.emoji-wrestlers_tone5 { background-position: -840px -240px; }
+.emoji-writing_hand { background-position: -840px -260px; }
+.emoji-writing_hand_tone1 { background-position: -840px -280px; }
+.emoji-writing_hand_tone2 { background-position: -840px -300px; }
+.emoji-writing_hand_tone3 { background-position: -840px -320px; }
+.emoji-writing_hand_tone4 { background-position: -840px -340px; }
+.emoji-writing_hand_tone5 { background-position: -840px -360px; }
+.emoji-x { background-position: -840px -380px; }
+.emoji-yellow_heart { background-position: -840px -400px; }
+.emoji-yen { background-position: -840px -420px; }
+.emoji-yin_yang { background-position: -840px -440px; }
+.emoji-yum { background-position: -840px -460px; }
+.emoji-zap { background-position: -840px -480px; }
+.emoji-zero { background-position: -840px -500px; }
+.emoji-zipper_mouth { background-position: -840px -520px; }
+.emoji-100 { background-position: -840px -540px; }
+
+.emoji-icon {
+  background-image: image-url('emoji.png');
+  background-repeat: no-repeat;
+  color: transparent;
+  text-indent: -99em;
+  height: 20px;
+  width: 20px;
+
+  @media only screen and (-webkit-min-device-pixel-ratio: 2),
+         only screen and (min--moz-device-pixel-ratio: 2),
+         only screen and (-o-min-device-pixel-ratio: 2/1),
+         only screen and (min-device-pixel-ratio: 2),
+         only screen and (min-resolution: 192dpi),
+         only screen and (min-resolution: 2dppx) {
+    background-image: image-url('emoji@2x.png');
+    background-size: 860px 840px;
+  }
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 7158de65143003079fef00eb00a77339c7a8230e..d86ae57cd9ac577dc8b1a04db656c7cb6b275857 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,1809 +1,7 @@
-.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; }
-.emoji-0032-20E3 { background-position: -40px 0; }
-.emoji-0033-20E3 { background-position: -40px -20px; }
-.emoji-0034-20E3 { background-position: 0 -40px; }
-.emoji-0035-20E3 { background-position: -20px -40px; }
-.emoji-0036-20E3 { background-position: -40px -40px; }
-.emoji-0037-20E3 { background-position: -60px 0; }
-.emoji-0038-20E3 { background-position: -60px -20px; }
-.emoji-0039-20E3 { background-position: -60px -40px; }
-.emoji-00A9 { background-position: 0 -60px; }
-.emoji-00AE { background-position: -20px -60px; }
-.emoji-1F004 { background-position: -40px -60px; }
-.emoji-1F0CF { background-position: -60px -60px; }
-.emoji-1F170 { background-position: -80px 0; }
-.emoji-1F171 { background-position: -80px -20px; }
-.emoji-1F17E { background-position: -80px -40px; }
-.emoji-1F17F { background-position: -80px -60px; }
-.emoji-1F18E { background-position: 0 -80px; }
-.emoji-1F191 { background-position: -20px -80px; }
-.emoji-1F192 { background-position: -40px -80px; }
-.emoji-1F193 { background-position: -60px -80px; }
-.emoji-1F194 { background-position: -80px -80px; }
-.emoji-1F195 { background-position: -100px 0; }
-.emoji-1F196 { background-position: -100px -20px; }
-.emoji-1F197 { background-position: -100px -40px; }
-.emoji-1F198 { background-position: -100px -60px; }
-.emoji-1F199 { background-position: -100px -80px; }
-.emoji-1F19A { background-position: 0 -100px; }
-.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
-.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
-.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
-.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
-.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
-.emoji-1F1E6-1F1EE { background-position: -120px 0; }
-.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
-.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
-.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
-.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
-.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
-.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
-.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
-.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
-.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
-.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
-.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
-.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
-.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
-.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
-.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
-.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
-.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
-.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
-.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
-.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
-.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
-.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
-.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
-.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
-.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
-.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
-.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
-.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
-.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
-.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
-.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
-.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
-.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
-.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
-.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
-.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
-.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
-.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
-.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
-.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
-.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
-.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
-.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
-.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
-.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
-.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
-.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
-.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
-.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
-.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
-.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
-.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
-.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
-.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
-.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
-.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
-.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
-.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
-.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
-.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
-.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
-.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
-.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
-.emoji-1F1EA-1F1ED { background-position: -200px 0; }
-.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
-.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
-.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
-.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
-.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
-.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
-.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
-.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
-.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
-.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
-.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
-.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
-.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
-.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
-.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
-.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
-.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
-.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
-.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
-.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
-.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
-.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
-.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
-.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
-.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
-.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
-.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
-.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
-.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
-.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
-.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
-.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
-.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
-.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
-.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
-.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
-.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
-.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
-.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
-.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
-.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
-.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
-.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
-.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
-.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
-.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
-.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
-.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
-.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
-.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
-.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
-.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
-.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
-.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
-.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
-.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
-.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
-.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
-.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
-.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
-.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
-.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
-.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
-.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
-.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
-.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
-.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
-.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
-.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
-.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
-.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
-.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
-.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
-.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
-.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
-.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
-.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
-.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
-.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
-.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
-.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
-.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
-.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
-.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
-.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
-.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
-.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
-.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
-.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
-.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
-.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
-.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
-.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
-.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
-.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
-.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
-.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
-.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
-.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
-.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
-.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
-.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
-.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
-.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
-.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
-.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
-.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
-.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
-.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
-.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
-.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
-.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
-.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
-.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
-.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
-.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
-.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
-.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
-.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
-.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
-.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
-.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
-.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
-.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
-.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
-.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
-.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
-.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
-.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
-.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
-.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
-.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
-.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
-.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
-.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
-.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
-.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
-.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
-.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
-.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
-.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
-.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
-.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
-.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
-.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
-.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
-.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
-.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
-.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
-.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
-.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
-.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
-.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
-.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
-.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
-.emoji-1F1F9-1F1EF { background-position: -320px 0; }
-.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
-.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
-.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
-.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
-.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
-.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
-.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
-.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
-.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
-.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
-.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
-.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
-.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
-.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
-.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
-.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
-.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
-.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
-.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
-.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
-.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
-.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
-.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
-.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
-.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
-.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
-.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
-.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
-.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
-.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
-.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
-.emoji-1F201 { background-position: -320px -320px; }
-.emoji-1F202 { background-position: -340px 0; }
-.emoji-1F21A { background-position: -340px -20px; }
-.emoji-1F22F { background-position: -340px -40px; }
-.emoji-1F232 { background-position: -340px -60px; }
-.emoji-1F233 { background-position: -340px -80px; }
-.emoji-1F234 { background-position: -340px -100px; }
-.emoji-1F235 { background-position: -340px -120px; }
-.emoji-1F236 { background-position: -340px -140px; }
-.emoji-1F237 { background-position: -340px -160px; }
-.emoji-1F238 { background-position: -340px -180px; }
-.emoji-1F239 { background-position: -340px -200px; }
-.emoji-1F23A { background-position: -340px -220px; }
-.emoji-1F250 { background-position: -340px -240px; }
-.emoji-1F251 { background-position: -340px -260px; }
-.emoji-1F300 { background-position: -340px -280px; }
-.emoji-1F301 { background-position: -340px -300px; }
-.emoji-1F302 { background-position: -340px -320px; }
-.emoji-1F303 { background-position: 0 -340px; }
-.emoji-1F304 { background-position: -20px -340px; }
-.emoji-1F305 { background-position: -40px -340px; }
-.emoji-1F306 { background-position: -60px -340px; }
-.emoji-1F307 { background-position: -80px -340px; }
-.emoji-1F308 { background-position: -100px -340px; }
-.emoji-1F309 { background-position: -120px -340px; }
-.emoji-1F30A { background-position: -140px -340px; }
-.emoji-1F30B { background-position: -160px -340px; }
-.emoji-1F30C { background-position: -180px -340px; }
-.emoji-1F30D { background-position: -200px -340px; }
-.emoji-1F30E { background-position: -220px -340px; }
-.emoji-1F30F { background-position: -240px -340px; }
-.emoji-1F310 { background-position: -260px -340px; }
-.emoji-1F311 { background-position: -280px -340px; }
-.emoji-1F312 { background-position: -300px -340px; }
-.emoji-1F313 { background-position: -320px -340px; }
-.emoji-1F314 { background-position: -340px -340px; }
-.emoji-1F315 { background-position: -360px 0; }
-.emoji-1F316 { background-position: -360px -20px; }
-.emoji-1F317 { background-position: -360px -40px; }
-.emoji-1F318 { background-position: -360px -60px; }
-.emoji-1F319 { background-position: -360px -80px; }
-.emoji-1F31A { background-position: -360px -100px; }
-.emoji-1F31B { background-position: -360px -120px; }
-.emoji-1F31C { background-position: -360px -140px; }
-.emoji-1F31D { background-position: -360px -160px; }
-.emoji-1F31E { background-position: -360px -180px; }
-.emoji-1F31F { background-position: -360px -200px; }
-.emoji-1F320 { background-position: -360px -220px; }
-.emoji-1F321 { background-position: -360px -240px; }
-.emoji-1F324 { background-position: -360px -260px; }
-.emoji-1F325 { background-position: -360px -280px; }
-.emoji-1F326 { background-position: -360px -300px; }
-.emoji-1F327 { background-position: -360px -320px; }
-.emoji-1F328 { background-position: -360px -340px; }
-.emoji-1F329 { background-position: 0 -360px; }
-.emoji-1F32A { background-position: -20px -360px; }
-.emoji-1F32B { background-position: -40px -360px; }
-.emoji-1F32C { background-position: -60px -360px; }
-.emoji-1F32D { background-position: -80px -360px; }
-.emoji-1F32E { background-position: -100px -360px; }
-.emoji-1F32F { background-position: -120px -360px; }
-.emoji-1F330 { background-position: -140px -360px; }
-.emoji-1F331 { background-position: -160px -360px; }
-.emoji-1F332 { background-position: -180px -360px; }
-.emoji-1F333 { background-position: -200px -360px; }
-.emoji-1F334 { background-position: -220px -360px; }
-.emoji-1F335 { background-position: -240px -360px; }
-.emoji-1F336 { background-position: -260px -360px; }
-.emoji-1F337 { background-position: -280px -360px; }
-.emoji-1F338 { background-position: -300px -360px; }
-.emoji-1F339 { background-position: -320px -360px; }
-.emoji-1F33A { background-position: -340px -360px; }
-.emoji-1F33B { background-position: -360px -360px; }
-.emoji-1F33C { background-position: -380px 0; }
-.emoji-1F33D { background-position: -380px -20px; }
-.emoji-1F33E { background-position: -380px -40px; }
-.emoji-1F33F { background-position: -380px -60px; }
-.emoji-1F340 { background-position: -380px -80px; }
-.emoji-1F341 { background-position: -380px -100px; }
-.emoji-1F342 { background-position: -380px -120px; }
-.emoji-1F343 { background-position: -380px -140px; }
-.emoji-1F344 { background-position: -380px -160px; }
-.emoji-1F345 { background-position: -380px -180px; }
-.emoji-1F346 { background-position: -380px -200px; }
-.emoji-1F347 { background-position: -380px -220px; }
-.emoji-1F348 { background-position: -380px -240px; }
-.emoji-1F349 { background-position: -380px -260px; }
-.emoji-1F34A { background-position: -380px -280px; }
-.emoji-1F34B { background-position: -380px -300px; }
-.emoji-1F34C { background-position: -380px -320px; }
-.emoji-1F34D { background-position: -380px -340px; }
-.emoji-1F34E { background-position: -380px -360px; }
-.emoji-1F34F { background-position: 0 -380px; }
-.emoji-1F350 { background-position: -20px -380px; }
-.emoji-1F351 { background-position: -40px -380px; }
-.emoji-1F352 { background-position: -60px -380px; }
-.emoji-1F353 { background-position: -80px -380px; }
-.emoji-1F354 { background-position: -100px -380px; }
-.emoji-1F355 { background-position: -120px -380px; }
-.emoji-1F356 { background-position: -140px -380px; }
-.emoji-1F357 { background-position: -160px -380px; }
-.emoji-1F358 { background-position: -180px -380px; }
-.emoji-1F359 { background-position: -200px -380px; }
-.emoji-1F35A { background-position: -220px -380px; }
-.emoji-1F35B { background-position: -240px -380px; }
-.emoji-1F35C { background-position: -260px -380px; }
-.emoji-1F35D { background-position: -280px -380px; }
-.emoji-1F35E { background-position: -300px -380px; }
-.emoji-1F35F { background-position: -320px -380px; }
-.emoji-1F360 { background-position: -340px -380px; }
-.emoji-1F361 { background-position: -360px -380px; }
-.emoji-1F362 { background-position: -380px -380px; }
-.emoji-1F363 { background-position: -400px 0; }
-.emoji-1F364 { background-position: -400px -20px; }
-.emoji-1F365 { background-position: -400px -40px; }
-.emoji-1F366 { background-position: -400px -60px; }
-.emoji-1F367 { background-position: -400px -80px; }
-.emoji-1F368 { background-position: -400px -100px; }
-.emoji-1F369 { background-position: -400px -120px; }
-.emoji-1F36A { background-position: -400px -140px; }
-.emoji-1F36B { background-position: -400px -160px; }
-.emoji-1F36C { background-position: -400px -180px; }
-.emoji-1F36D { background-position: -400px -200px; }
-.emoji-1F36E { background-position: -400px -220px; }
-.emoji-1F36F { background-position: -400px -240px; }
-.emoji-1F370 { background-position: -400px -260px; }
-.emoji-1F371 { background-position: -400px -280px; }
-.emoji-1F372 { background-position: -400px -300px; }
-.emoji-1F373 { background-position: -400px -320px; }
-.emoji-1F374 { background-position: -400px -340px; }
-.emoji-1F375 { background-position: -400px -360px; }
-.emoji-1F376 { background-position: -400px -380px; }
-.emoji-1F377 { background-position: 0 -400px; }
-.emoji-1F378 { background-position: -20px -400px; }
-.emoji-1F379 { background-position: -40px -400px; }
-.emoji-1F37A { background-position: -60px -400px; }
-.emoji-1F37B { background-position: -80px -400px; }
-.emoji-1F37C { background-position: -100px -400px; }
-.emoji-1F37D { background-position: -120px -400px; }
-.emoji-1F37E { background-position: -140px -400px; }
-.emoji-1F37F { background-position: -160px -400px; }
-.emoji-1F380 { background-position: -180px -400px; }
-.emoji-1F381 { background-position: -200px -400px; }
-.emoji-1F382 { background-position: -220px -400px; }
-.emoji-1F383 { background-position: -240px -400px; }
-.emoji-1F384 { background-position: -260px -400px; }
-.emoji-1F385 { background-position: -280px -400px; }
-.emoji-1F385-1F3FB { background-position: -300px -400px; }
-.emoji-1F385-1F3FC { background-position: -320px -400px; }
-.emoji-1F385-1F3FD { background-position: -340px -400px; }
-.emoji-1F385-1F3FE { background-position: -360px -400px; }
-.emoji-1F385-1F3FF { background-position: -380px -400px; }
-.emoji-1F386 { background-position: -400px -400px; }
-.emoji-1F387 { background-position: -420px 0; }
-.emoji-1F388 { background-position: -420px -20px; }
-.emoji-1F389 { background-position: -420px -40px; }
-.emoji-1F38A { background-position: -420px -60px; }
-.emoji-1F38B { background-position: -420px -80px; }
-.emoji-1F38C { background-position: -420px -100px; }
-.emoji-1F38D { background-position: -420px -120px; }
-.emoji-1F38E { background-position: -420px -140px; }
-.emoji-1F38F { background-position: -420px -160px; }
-.emoji-1F390 { background-position: -420px -180px; }
-.emoji-1F391 { background-position: -420px -200px; }
-.emoji-1F392 { background-position: -420px -220px; }
-.emoji-1F393 { background-position: -420px -240px; }
-.emoji-1F396 { background-position: -420px -260px; }
-.emoji-1F397 { background-position: -420px -280px; }
-.emoji-1F399 { background-position: -420px -300px; }
-.emoji-1F39A { background-position: -420px -320px; }
-.emoji-1F39B { background-position: -420px -340px; }
-.emoji-1F39E { background-position: -420px -360px; }
-.emoji-1F39F { background-position: -420px -380px; }
-.emoji-1F3A0 { background-position: -420px -400px; }
-.emoji-1F3A1 { background-position: 0 -420px; }
-.emoji-1F3A2 { background-position: -20px -420px; }
-.emoji-1F3A3 { background-position: -40px -420px; }
-.emoji-1F3A4 { background-position: -60px -420px; }
-.emoji-1F3A5 { background-position: -80px -420px; }
-.emoji-1F3A6 { background-position: -100px -420px; }
-.emoji-1F3A7 { background-position: -120px -420px; }
-.emoji-1F3A8 { background-position: -140px -420px; }
-.emoji-1F3A9 { background-position: -160px -420px; }
-.emoji-1F3AA { background-position: -180px -420px; }
-.emoji-1F3AB { background-position: -200px -420px; }
-.emoji-1F3AC { background-position: -220px -420px; }
-.emoji-1F3AD { background-position: -240px -420px; }
-.emoji-1F3AE { background-position: -260px -420px; }
-.emoji-1F3AF { background-position: -280px -420px; }
-.emoji-1F3B0 { background-position: -300px -420px; }
-.emoji-1F3B1 { background-position: -320px -420px; }
-.emoji-1F3B2 { background-position: -340px -420px; }
-.emoji-1F3B3 { background-position: -360px -420px; }
-.emoji-1F3B4 { background-position: -380px -420px; }
-.emoji-1F3B5 { background-position: -400px -420px; }
-.emoji-1F3B6 { background-position: -420px -420px; }
-.emoji-1F3B7 { background-position: -440px 0; }
-.emoji-1F3B8 { background-position: -440px -20px; }
-.emoji-1F3B9 { background-position: -440px -40px; }
-.emoji-1F3BA { background-position: -440px -60px; }
-.emoji-1F3BB { background-position: -440px -80px; }
-.emoji-1F3BC { background-position: -440px -100px; }
-.emoji-1F3BD { background-position: -440px -120px; }
-.emoji-1F3BE { background-position: -440px -140px; }
-.emoji-1F3BF { background-position: -440px -160px; }
-.emoji-1F3C0 { background-position: -440px -180px; }
-.emoji-1F3C1 { background-position: -440px -200px; }
-.emoji-1F3C2 { background-position: -440px -220px; }
-.emoji-1F3C3 { background-position: -440px -240px; }
-.emoji-1F3C3-1F3FB { background-position: -440px -260px; }
-.emoji-1F3C3-1F3FC { background-position: -440px -280px; }
-.emoji-1F3C3-1F3FD { background-position: -440px -300px; }
-.emoji-1F3C3-1F3FE { background-position: -440px -320px; }
-.emoji-1F3C3-1F3FF { background-position: -440px -340px; }
-.emoji-1F3C4 { background-position: -440px -360px; }
-.emoji-1F3C4-1F3FB { background-position: -440px -380px; }
-.emoji-1F3C4-1F3FC { background-position: -440px -400px; }
-.emoji-1F3C4-1F3FD { background-position: -440px -420px; }
-.emoji-1F3C4-1F3FE { background-position: 0 -440px; }
-.emoji-1F3C4-1F3FF { background-position: -20px -440px; }
-.emoji-1F3C5 { background-position: -40px -440px; }
-.emoji-1F3C6 { background-position: -60px -440px; }
-.emoji-1F3C7 { background-position: -80px -440px; }
-.emoji-1F3C7-1F3FB { background-position: -100px -440px; }
-.emoji-1F3C7-1F3FC { background-position: -120px -440px; }
-.emoji-1F3C7-1F3FD { background-position: -140px -440px; }
-.emoji-1F3C7-1F3FE { background-position: -160px -440px; }
-.emoji-1F3C7-1F3FF { background-position: -180px -440px; }
-.emoji-1F3C8 { background-position: -200px -440px; }
-.emoji-1F3C9 { background-position: -220px -440px; }
-.emoji-1F3CA { background-position: -240px -440px; }
-.emoji-1F3CA-1F3FB { background-position: -260px -440px; }
-.emoji-1F3CA-1F3FC { background-position: -280px -440px; }
-.emoji-1F3CA-1F3FD { background-position: -300px -440px; }
-.emoji-1F3CA-1F3FE { background-position: -320px -440px; }
-.emoji-1F3CA-1F3FF { background-position: -340px -440px; }
-.emoji-1F3CB { background-position: -360px -440px; }
-.emoji-1F3CB-1F3FB { background-position: -380px -440px; }
-.emoji-1F3CB-1F3FC { background-position: -400px -440px; }
-.emoji-1F3CB-1F3FD { background-position: -420px -440px; }
-.emoji-1F3CB-1F3FE { background-position: -440px -440px; }
-.emoji-1F3CB-1F3FF { background-position: -460px 0; }
-.emoji-1F3CC { background-position: -460px -20px; }
-.emoji-1F3CD { background-position: -460px -40px; }
-.emoji-1F3CE { background-position: -460px -60px; }
-.emoji-1F3CF { background-position: -460px -80px; }
-.emoji-1F3D0 { background-position: -460px -100px; }
-.emoji-1F3D1 { background-position: -460px -120px; }
-.emoji-1F3D2 { background-position: -460px -140px; }
-.emoji-1F3D3 { background-position: -460px -160px; }
-.emoji-1F3D4 { background-position: -460px -180px; }
-.emoji-1F3D5 { background-position: -460px -200px; }
-.emoji-1F3D6 { background-position: -460px -220px; }
-.emoji-1F3D7 { background-position: -460px -240px; }
-.emoji-1F3D8 { background-position: -460px -260px; }
-.emoji-1F3D9 { background-position: -460px -280px; }
-.emoji-1F3DA { background-position: -460px -300px; }
-.emoji-1F3DB { background-position: -460px -320px; }
-.emoji-1F3DC { background-position: -460px -340px; }
-.emoji-1F3DD { background-position: -460px -360px; }
-.emoji-1F3DE { background-position: -460px -380px; }
-.emoji-1F3DF { background-position: -460px -400px; }
-.emoji-1F3E0 { background-position: -460px -420px; }
-.emoji-1F3E1 { background-position: -460px -440px; }
-.emoji-1F3E2 { background-position: 0 -460px; }
-.emoji-1F3E3 { background-position: -20px -460px; }
-.emoji-1F3E4 { background-position: -40px -460px; }
-.emoji-1F3E5 { background-position: -60px -460px; }
-.emoji-1F3E6 { background-position: -80px -460px; }
-.emoji-1F3E7 { background-position: -100px -460px; }
-.emoji-1F3E8 { background-position: -120px -460px; }
-.emoji-1F3E9 { background-position: -140px -460px; }
-.emoji-1F3EA { background-position: -160px -460px; }
-.emoji-1F3EB { background-position: -180px -460px; }
-.emoji-1F3EC { background-position: -200px -460px; }
-.emoji-1F3ED { background-position: -220px -460px; }
-.emoji-1F3EE { background-position: -240px -460px; }
-.emoji-1F3EF { background-position: -260px -460px; }
-.emoji-1F3F0 { background-position: -280px -460px; }
-.emoji-1F3F3 { background-position: -300px -460px; }
-.emoji-1F3F4 { background-position: -320px -460px; }
-.emoji-1F3F5 { background-position: -340px -460px; }
-.emoji-1F3F7 { background-position: -360px -460px; }
-.emoji-1F3F8 { background-position: -380px -460px; }
-.emoji-1F3F9 { background-position: -400px -460px; }
-.emoji-1F3FA { background-position: -420px -460px; }
-.emoji-1F3FB { background-position: -440px -460px; }
-.emoji-1F3FC { background-position: -460px -460px; }
-.emoji-1F3FD { background-position: -480px 0; }
-.emoji-1F3FE { background-position: -480px -20px; }
-.emoji-1F3FF { background-position: -480px -40px; }
-.emoji-1F400 { background-position: -480px -60px; }
-.emoji-1F401 { background-position: -480px -80px; }
-.emoji-1F402 { background-position: -480px -100px; }
-.emoji-1F403 { background-position: -480px -120px; }
-.emoji-1F404 { background-position: -480px -140px; }
-.emoji-1F405 { background-position: -480px -160px; }
-.emoji-1F406 { background-position: -480px -180px; }
-.emoji-1F407 { background-position: -480px -200px; }
-.emoji-1F408 { background-position: -480px -220px; }
-.emoji-1F409 { background-position: -480px -240px; }
-.emoji-1F40A { background-position: -480px -260px; }
-.emoji-1F40B { background-position: -480px -280px; }
-.emoji-1F40C { background-position: -480px -300px; }
-.emoji-1F40D { background-position: -480px -320px; }
-.emoji-1F40E { background-position: -480px -340px; }
-.emoji-1F40F { background-position: -480px -360px; }
-.emoji-1F410 { background-position: -480px -380px; }
-.emoji-1F411 { background-position: -480px -400px; }
-.emoji-1F412 { background-position: -480px -420px; }
-.emoji-1F413 { background-position: -480px -440px; }
-.emoji-1F414 { background-position: -480px -460px; }
-.emoji-1F415 { background-position: 0 -480px; }
-.emoji-1F416 { background-position: -20px -480px; }
-.emoji-1F417 { background-position: -40px -480px; }
-.emoji-1F418 { background-position: -60px -480px; }
-.emoji-1F419 { background-position: -80px -480px; }
-.emoji-1F41A { background-position: -100px -480px; }
-.emoji-1F41B { background-position: -120px -480px; }
-.emoji-1F41C { background-position: -140px -480px; }
-.emoji-1F41D { background-position: -160px -480px; }
-.emoji-1F41E { background-position: -180px -480px; }
-.emoji-1F41F { background-position: -200px -480px; }
-.emoji-1F420 { background-position: -220px -480px; }
-.emoji-1F421 { background-position: -240px -480px; }
-.emoji-1F422 { background-position: -260px -480px; }
-.emoji-1F423 { background-position: -280px -480px; }
-.emoji-1F424 { background-position: -300px -480px; }
-.emoji-1F425 { background-position: -320px -480px; }
-.emoji-1F426 { background-position: -340px -480px; }
-.emoji-1F427 { background-position: -360px -480px; }
-.emoji-1F428 { background-position: -380px -480px; }
-.emoji-1F429 { background-position: -400px -480px; }
-.emoji-1F42A { background-position: -420px -480px; }
-.emoji-1F42B { background-position: -440px -480px; }
-.emoji-1F42C { background-position: -460px -480px; }
-.emoji-1F42D { background-position: -480px -480px; }
-.emoji-1F42E { background-position: -500px 0; }
-.emoji-1F42F { background-position: -500px -20px; }
-.emoji-1F430 { background-position: -500px -40px; }
-.emoji-1F431 { background-position: -500px -60px; }
-.emoji-1F432 { background-position: -500px -80px; }
-.emoji-1F433 { background-position: -500px -100px; }
-.emoji-1F434 { background-position: -500px -120px; }
-.emoji-1F435 { background-position: -500px -140px; }
-.emoji-1F436 { background-position: -500px -160px; }
-.emoji-1F437 { background-position: -500px -180px; }
-.emoji-1F438 { background-position: -500px -200px; }
-.emoji-1F439 { background-position: -500px -220px; }
-.emoji-1F43A { background-position: -500px -240px; }
-.emoji-1F43B { background-position: -500px -260px; }
-.emoji-1F43C { background-position: -500px -280px; }
-.emoji-1F43D { background-position: -500px -300px; }
-.emoji-1F43E { background-position: -500px -320px; }
-.emoji-1F43F { background-position: -500px -340px; }
-.emoji-1F440 { background-position: -500px -360px; }
-.emoji-1F441 { background-position: -500px -380px; }
-.emoji-1F441-1F5E8 { background-position: -500px -400px; }
-.emoji-1F442 { background-position: -500px -420px; }
-.emoji-1F442-1F3FB { background-position: -500px -440px; }
-.emoji-1F442-1F3FC { background-position: -500px -460px; }
-.emoji-1F442-1F3FD { background-position: -500px -480px; }
-.emoji-1F442-1F3FE { background-position: 0 -500px; }
-.emoji-1F442-1F3FF { background-position: -20px -500px; }
-.emoji-1F443 { background-position: -40px -500px; }
-.emoji-1F443-1F3FB { background-position: -60px -500px; }
-.emoji-1F443-1F3FC { background-position: -80px -500px; }
-.emoji-1F443-1F3FD { background-position: -100px -500px; }
-.emoji-1F443-1F3FE { background-position: -120px -500px; }
-.emoji-1F443-1F3FF { background-position: -140px -500px; }
-.emoji-1F444 { background-position: -160px -500px; }
-.emoji-1F445 { background-position: -180px -500px; }
-.emoji-1F446 { background-position: -200px -500px; }
-.emoji-1F446-1F3FB { background-position: -220px -500px; }
-.emoji-1F446-1F3FC { background-position: -240px -500px; }
-.emoji-1F446-1F3FD { background-position: -260px -500px; }
-.emoji-1F446-1F3FE { background-position: -280px -500px; }
-.emoji-1F446-1F3FF { background-position: -300px -500px; }
-.emoji-1F447 { background-position: -320px -500px; }
-.emoji-1F447-1F3FB { background-position: -340px -500px; }
-.emoji-1F447-1F3FC { background-position: -360px -500px; }
-.emoji-1F447-1F3FD { background-position: -380px -500px; }
-.emoji-1F447-1F3FE { background-position: -400px -500px; }
-.emoji-1F447-1F3FF { background-position: -420px -500px; }
-.emoji-1F448 { background-position: -440px -500px; }
-.emoji-1F448-1F3FB { background-position: -460px -500px; }
-.emoji-1F448-1F3FC { background-position: -480px -500px; }
-.emoji-1F448-1F3FD { background-position: -500px -500px; }
-.emoji-1F448-1F3FE { background-position: -520px 0; }
-.emoji-1F448-1F3FF { background-position: -520px -20px; }
-.emoji-1F449 { background-position: -520px -40px; }
-.emoji-1F449-1F3FB { background-position: -520px -60px; }
-.emoji-1F449-1F3FC { background-position: -520px -80px; }
-.emoji-1F449-1F3FD { background-position: -520px -100px; }
-.emoji-1F449-1F3FE { background-position: -520px -120px; }
-.emoji-1F449-1F3FF { background-position: -520px -140px; }
-.emoji-1F44A { background-position: -520px -160px; }
-.emoji-1F44A-1F3FB { background-position: -520px -180px; }
-.emoji-1F44A-1F3FC { background-position: -520px -200px; }
-.emoji-1F44A-1F3FD { background-position: -520px -220px; }
-.emoji-1F44A-1F3FE { background-position: -520px -240px; }
-.emoji-1F44A-1F3FF { background-position: -520px -260px; }
-.emoji-1F44B { background-position: -520px -280px; }
-.emoji-1F44B-1F3FB { background-position: -520px -300px; }
-.emoji-1F44B-1F3FC { background-position: -520px -320px; }
-.emoji-1F44B-1F3FD { background-position: -520px -340px; }
-.emoji-1F44B-1F3FE { background-position: -520px -360px; }
-.emoji-1F44B-1F3FF { background-position: -520px -380px; }
-.emoji-1F44C { background-position: -520px -400px; }
-.emoji-1F44C-1F3FB { background-position: -520px -420px; }
-.emoji-1F44C-1F3FC { background-position: -520px -440px; }
-.emoji-1F44C-1F3FD { background-position: -520px -460px; }
-.emoji-1F44C-1F3FE { background-position: -520px -480px; }
-.emoji-1F44C-1F3FF { background-position: -520px -500px; }
-.emoji-1F44D { background-position: 0 -520px; }
-.emoji-1F44D-1F3FB { background-position: -20px -520px; }
-.emoji-1F44D-1F3FC { background-position: -40px -520px; }
-.emoji-1F44D-1F3FD { background-position: -60px -520px; }
-.emoji-1F44D-1F3FE { background-position: -80px -520px; }
-.emoji-1F44D-1F3FF { background-position: -100px -520px; }
-.emoji-1F44E { background-position: -120px -520px; }
-.emoji-1F44E-1F3FB { background-position: -140px -520px; }
-.emoji-1F44E-1F3FC { background-position: -160px -520px; }
-.emoji-1F44E-1F3FD { background-position: -180px -520px; }
-.emoji-1F44E-1F3FE { background-position: -200px -520px; }
-.emoji-1F44E-1F3FF { background-position: -220px -520px; }
-.emoji-1F44F { background-position: -240px -520px; }
-.emoji-1F44F-1F3FB { background-position: -260px -520px; }
-.emoji-1F44F-1F3FC { background-position: -280px -520px; }
-.emoji-1F44F-1F3FD { background-position: -300px -520px; }
-.emoji-1F44F-1F3FE { background-position: -320px -520px; }
-.emoji-1F44F-1F3FF { background-position: -340px -520px; }
-.emoji-1F450 { background-position: -360px -520px; }
-.emoji-1F450-1F3FB { background-position: -380px -520px; }
-.emoji-1F450-1F3FC { background-position: -400px -520px; }
-.emoji-1F450-1F3FD { background-position: -420px -520px; }
-.emoji-1F450-1F3FE { background-position: -440px -520px; }
-.emoji-1F450-1F3FF { background-position: -460px -520px; }
-.emoji-1F451 { background-position: -480px -520px; }
-.emoji-1F452 { background-position: -500px -520px; }
-.emoji-1F453 { background-position: -520px -520px; }
-.emoji-1F454 { background-position: -540px 0; }
-.emoji-1F455 { background-position: -540px -20px; }
-.emoji-1F456 { background-position: -540px -40px; }
-.emoji-1F457 { background-position: -540px -60px; }
-.emoji-1F458 { background-position: -540px -80px; }
-.emoji-1F459 { background-position: -540px -100px; }
-.emoji-1F45A { background-position: -540px -120px; }
-.emoji-1F45B { background-position: -540px -140px; }
-.emoji-1F45C { background-position: -540px -160px; }
-.emoji-1F45D { background-position: -540px -180px; }
-.emoji-1F45E { background-position: -540px -200px; }
-.emoji-1F45F { background-position: -540px -220px; }
-.emoji-1F460 { background-position: -540px -240px; }
-.emoji-1F461 { background-position: -540px -260px; }
-.emoji-1F462 { background-position: -540px -280px; }
-.emoji-1F463 { background-position: -540px -300px; }
-.emoji-1F464 { background-position: -540px -320px; }
-.emoji-1F465 { background-position: -540px -340px; }
-.emoji-1F466 { background-position: -540px -360px; }
-.emoji-1F466-1F3FB { background-position: -540px -380px; }
-.emoji-1F466-1F3FC { background-position: -540px -400px; }
-.emoji-1F466-1F3FD { background-position: -540px -420px; }
-.emoji-1F466-1F3FE { background-position: -540px -440px; }
-.emoji-1F466-1F3FF { background-position: -540px -460px; }
-.emoji-1F467 { background-position: -540px -480px; }
-.emoji-1F467-1F3FB { background-position: -540px -500px; }
-.emoji-1F467-1F3FC { background-position: -540px -520px; }
-.emoji-1F467-1F3FD { background-position: 0 -540px; }
-.emoji-1F467-1F3FE { background-position: -20px -540px; }
-.emoji-1F467-1F3FF { background-position: -40px -540px; }
-.emoji-1F468 { background-position: -60px -540px; }
-.emoji-1F468-1F3FB { background-position: -80px -540px; }
-.emoji-1F468-1F3FC { background-position: -100px -540px; }
-.emoji-1F468-1F3FD { background-position: -120px -540px; }
-.emoji-1F468-1F3FE { background-position: -140px -540px; }
-.emoji-1F468-1F3FF { background-position: -160px -540px; }
-.emoji-1F468-1F468-1F466 { background-position: -180px -540px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; }
-.emoji-1F468-1F468-1F467 { background-position: -220px -540px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; }
-.emoji-1F468-1F469-1F467 { background-position: -300px -540px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; }
-.emoji-1F468-2764-1F468 { background-position: -360px -540px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; }
-.emoji-1F469 { background-position: -400px -540px; }
-.emoji-1F469-1F3FB { background-position: -420px -540px; }
-.emoji-1F469-1F3FC { background-position: -440px -540px; }
-.emoji-1F469-1F3FD { background-position: -460px -540px; }
-.emoji-1F469-1F3FE { background-position: -480px -540px; }
-.emoji-1F469-1F3FF { background-position: -500px -540px; }
-.emoji-1F469-1F469-1F466 { background-position: -520px -540px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; }
-.emoji-1F469-1F469-1F467 { background-position: -560px 0; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; }
-.emoji-1F469-2764-1F469 { background-position: -560px -60px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; }
-.emoji-1F46A { background-position: -560px -100px; }
-.emoji-1F46B { background-position: -560px -120px; }
-.emoji-1F46C { background-position: -560px -140px; }
-.emoji-1F46D { background-position: -560px -160px; }
-.emoji-1F46E { background-position: -560px -180px; }
-.emoji-1F46E-1F3FB { background-position: -560px -200px; }
-.emoji-1F46E-1F3FC { background-position: -560px -220px; }
-.emoji-1F46E-1F3FD { background-position: -560px -240px; }
-.emoji-1F46E-1F3FE { background-position: -560px -260px; }
-.emoji-1F46E-1F3FF { background-position: -560px -280px; }
-.emoji-1F46F { background-position: -560px -300px; }
-.emoji-1F470 { background-position: -560px -320px; }
-.emoji-1F470-1F3FB { background-position: -560px -340px; }
-.emoji-1F470-1F3FC { background-position: -560px -360px; }
-.emoji-1F470-1F3FD { background-position: -560px -380px; }
-.emoji-1F470-1F3FE { background-position: -560px -400px; }
-.emoji-1F470-1F3FF { background-position: -560px -420px; }
-.emoji-1F471 { background-position: -560px -440px; }
-.emoji-1F471-1F3FB { background-position: -560px -460px; }
-.emoji-1F471-1F3FC { background-position: -560px -480px; }
-.emoji-1F471-1F3FD { background-position: -560px -500px; }
-.emoji-1F471-1F3FE { background-position: -560px -520px; }
-.emoji-1F471-1F3FF { background-position: -560px -540px; }
-.emoji-1F472 { background-position: 0 -560px; }
-.emoji-1F472-1F3FB { background-position: -20px -560px; }
-.emoji-1F472-1F3FC { background-position: -40px -560px; }
-.emoji-1F472-1F3FD { background-position: -60px -560px; }
-.emoji-1F472-1F3FE { background-position: -80px -560px; }
-.emoji-1F472-1F3FF { background-position: -100px -560px; }
-.emoji-1F473 { background-position: -120px -560px; }
-.emoji-1F473-1F3FB { background-position: -140px -560px; }
-.emoji-1F473-1F3FC { background-position: -160px -560px; }
-.emoji-1F473-1F3FD { background-position: -180px -560px; }
-.emoji-1F473-1F3FE { background-position: -200px -560px; }
-.emoji-1F473-1F3FF { background-position: -220px -560px; }
-.emoji-1F474 { background-position: -240px -560px; }
-.emoji-1F474-1F3FB { background-position: -260px -560px; }
-.emoji-1F474-1F3FC { background-position: -280px -560px; }
-.emoji-1F474-1F3FD { background-position: -300px -560px; }
-.emoji-1F474-1F3FE { background-position: -320px -560px; }
-.emoji-1F474-1F3FF { background-position: -340px -560px; }
-.emoji-1F475 { background-position: -360px -560px; }
-.emoji-1F475-1F3FB { background-position: -380px -560px; }
-.emoji-1F475-1F3FC { background-position: -400px -560px; }
-.emoji-1F475-1F3FD { background-position: -420px -560px; }
-.emoji-1F475-1F3FE { background-position: -440px -560px; }
-.emoji-1F475-1F3FF { background-position: -460px -560px; }
-.emoji-1F476 { background-position: -480px -560px; }
-.emoji-1F476-1F3FB { background-position: -500px -560px; }
-.emoji-1F476-1F3FC { background-position: -520px -560px; }
-.emoji-1F476-1F3FD { background-position: -540px -560px; }
-.emoji-1F476-1F3FE { background-position: -560px -560px; }
-.emoji-1F476-1F3FF { background-position: -580px 0; }
-.emoji-1F477 { background-position: -580px -20px; }
-.emoji-1F477-1F3FB { background-position: -580px -40px; }
-.emoji-1F477-1F3FC { background-position: -580px -60px; }
-.emoji-1F477-1F3FD { background-position: -580px -80px; }
-.emoji-1F477-1F3FE { background-position: -580px -100px; }
-.emoji-1F477-1F3FF { background-position: -580px -120px; }
-.emoji-1F478 { background-position: -580px -140px; }
-.emoji-1F478-1F3FB { background-position: -580px -160px; }
-.emoji-1F478-1F3FC { background-position: -580px -180px; }
-.emoji-1F478-1F3FD { background-position: -580px -200px; }
-.emoji-1F478-1F3FE { background-position: -580px -220px; }
-.emoji-1F478-1F3FF { background-position: -580px -240px; }
-.emoji-1F479 { background-position: -580px -260px; }
-.emoji-1F47A { background-position: -580px -280px; }
-.emoji-1F47B { background-position: -580px -300px; }
-.emoji-1F47C { background-position: -580px -320px; }
-.emoji-1F47C-1F3FB { background-position: -580px -340px; }
-.emoji-1F47C-1F3FC { background-position: -580px -360px; }
-.emoji-1F47C-1F3FD { background-position: -580px -380px; }
-.emoji-1F47C-1F3FE { background-position: -580px -400px; }
-.emoji-1F47C-1F3FF { background-position: -580px -420px; }
-.emoji-1F47D { background-position: -580px -440px; }
-.emoji-1F47E { background-position: -580px -460px; }
-.emoji-1F47F { background-position: -580px -480px; }
-.emoji-1F480 { background-position: -580px -500px; }
-.emoji-1F481 { background-position: -580px -520px; }
-.emoji-1F481-1F3FB { background-position: -580px -540px; }
-.emoji-1F481-1F3FC { background-position: -580px -560px; }
-.emoji-1F481-1F3FD { background-position: 0 -580px; }
-.emoji-1F481-1F3FE { background-position: -20px -580px; }
-.emoji-1F481-1F3FF { background-position: -40px -580px; }
-.emoji-1F482 { background-position: -60px -580px; }
-.emoji-1F482-1F3FB { background-position: -80px -580px; }
-.emoji-1F482-1F3FC { background-position: -100px -580px; }
-.emoji-1F482-1F3FD { background-position: -120px -580px; }
-.emoji-1F482-1F3FE { background-position: -140px -580px; }
-.emoji-1F482-1F3FF { background-position: -160px -580px; }
-.emoji-1F483 { background-position: -180px -580px; }
-.emoji-1F483-1F3FB { background-position: -200px -580px; }
-.emoji-1F483-1F3FC { background-position: -220px -580px; }
-.emoji-1F483-1F3FD { background-position: -240px -580px; }
-.emoji-1F483-1F3FE { background-position: -260px -580px; }
-.emoji-1F483-1F3FF { background-position: -280px -580px; }
-.emoji-1F484 { background-position: -300px -580px; }
-.emoji-1F485 { background-position: -320px -580px; }
-.emoji-1F485-1F3FB { background-position: -340px -580px; }
-.emoji-1F485-1F3FC { background-position: -360px -580px; }
-.emoji-1F485-1F3FD { background-position: -380px -580px; }
-.emoji-1F485-1F3FE { background-position: -400px -580px; }
-.emoji-1F485-1F3FF { background-position: -420px -580px; }
-.emoji-1F486 { background-position: -440px -580px; }
-.emoji-1F486-1F3FB { background-position: -460px -580px; }
-.emoji-1F486-1F3FC { background-position: -480px -580px; }
-.emoji-1F486-1F3FD { background-position: -500px -580px; }
-.emoji-1F486-1F3FE { background-position: -520px -580px; }
-.emoji-1F486-1F3FF { background-position: -540px -580px; }
-.emoji-1F487 { background-position: -560px -580px; }
-.emoji-1F487-1F3FB { background-position: -580px -580px; }
-.emoji-1F487-1F3FC { background-position: -600px 0; }
-.emoji-1F487-1F3FD { background-position: -600px -20px; }
-.emoji-1F487-1F3FE { background-position: -600px -40px; }
-.emoji-1F487-1F3FF { background-position: -600px -60px; }
-.emoji-1F488 { background-position: -600px -80px; }
-.emoji-1F489 { background-position: -600px -100px; }
-.emoji-1F48A { background-position: -600px -120px; }
-.emoji-1F48B { background-position: -600px -140px; }
-.emoji-1F48C { background-position: -600px -160px; }
-.emoji-1F48D { background-position: -600px -180px; }
-.emoji-1F48E { background-position: -600px -200px; }
-.emoji-1F48F { background-position: -600px -220px; }
-.emoji-1F490 { background-position: -600px -240px; }
-.emoji-1F491 { background-position: -600px -260px; }
-.emoji-1F492 { background-position: -600px -280px; }
-.emoji-1F493 { background-position: -600px -300px; }
-.emoji-1F494 { background-position: -600px -320px; }
-.emoji-1F495 { background-position: -600px -340px; }
-.emoji-1F496 { background-position: -600px -360px; }
-.emoji-1F497 { background-position: -600px -380px; }
-.emoji-1F498 { background-position: -600px -400px; }
-.emoji-1F499 { background-position: -600px -420px; }
-.emoji-1F49A { background-position: -600px -440px; }
-.emoji-1F49B { background-position: -600px -460px; }
-.emoji-1F49C { background-position: -600px -480px; }
-.emoji-1F49D { background-position: -600px -500px; }
-.emoji-1F49E { background-position: -600px -520px; }
-.emoji-1F49F { background-position: -600px -540px; }
-.emoji-1F4A0 { background-position: -600px -560px; }
-.emoji-1F4A1 { background-position: -600px -580px; }
-.emoji-1F4A2 { background-position: 0 -600px; }
-.emoji-1F4A3 { background-position: -20px -600px; }
-.emoji-1F4A4 { background-position: -40px -600px; }
-.emoji-1F4A5 { background-position: -60px -600px; }
-.emoji-1F4A6 { background-position: -80px -600px; }
-.emoji-1F4A7 { background-position: -100px -600px; }
-.emoji-1F4A8 { background-position: -120px -600px; }
-.emoji-1F4A9 { background-position: -140px -600px; }
-.emoji-1F4AA { background-position: -160px -600px; }
-.emoji-1F4AA-1F3FB { background-position: -180px -600px; }
-.emoji-1F4AA-1F3FC { background-position: -200px -600px; }
-.emoji-1F4AA-1F3FD { background-position: -220px -600px; }
-.emoji-1F4AA-1F3FE { background-position: -240px -600px; }
-.emoji-1F4AA-1F3FF { background-position: -260px -600px; }
-.emoji-1F4AB { background-position: -280px -600px; }
-.emoji-1F4AC { background-position: -300px -600px; }
-.emoji-1F4AD { background-position: -320px -600px; }
-.emoji-1F4AE { background-position: -340px -600px; }
-.emoji-1F4AF { background-position: -360px -600px; }
-.emoji-1F4B0 { background-position: -380px -600px; }
-.emoji-1F4B1 { background-position: -400px -600px; }
-.emoji-1F4B2 { background-position: -420px -600px; }
-.emoji-1F4B3 { background-position: -440px -600px; }
-.emoji-1F4B4 { background-position: -460px -600px; }
-.emoji-1F4B5 { background-position: -480px -600px; }
-.emoji-1F4B6 { background-position: -500px -600px; }
-.emoji-1F4B7 { background-position: -520px -600px; }
-.emoji-1F4B8 { background-position: -540px -600px; }
-.emoji-1F4B9 { background-position: -560px -600px; }
-.emoji-1F4BA { background-position: -580px -600px; }
-.emoji-1F4BB { background-position: -600px -600px; }
-.emoji-1F4BC { background-position: -620px 0; }
-.emoji-1F4BD { background-position: -620px -20px; }
-.emoji-1F4BE { background-position: -620px -40px; }
-.emoji-1F4BF { background-position: -620px -60px; }
-.emoji-1F4C0 { background-position: -620px -80px; }
-.emoji-1F4C1 { background-position: -620px -100px; }
-.emoji-1F4C2 { background-position: -620px -120px; }
-.emoji-1F4C3 { background-position: -620px -140px; }
-.emoji-1F4C4 { background-position: -620px -160px; }
-.emoji-1F4C5 { background-position: -620px -180px; }
-.emoji-1F4C6 { background-position: -620px -200px; }
-.emoji-1F4C7 { background-position: -620px -220px; }
-.emoji-1F4C8 { background-position: -620px -240px; }
-.emoji-1F4C9 { background-position: -620px -260px; }
-.emoji-1F4CA { background-position: -620px -280px; }
-.emoji-1F4CB { background-position: -620px -300px; }
-.emoji-1F4CC { background-position: -620px -320px; }
-.emoji-1F4CD { background-position: -620px -340px; }
-.emoji-1F4CE { background-position: -620px -360px; }
-.emoji-1F4CF { background-position: -620px -380px; }
-.emoji-1F4D0 { background-position: -620px -400px; }
-.emoji-1F4D1 { background-position: -620px -420px; }
-.emoji-1F4D2 { background-position: -620px -440px; }
-.emoji-1F4D3 { background-position: -620px -460px; }
-.emoji-1F4D4 { background-position: -620px -480px; }
-.emoji-1F4D5 { background-position: -620px -500px; }
-.emoji-1F4D6 { background-position: -620px -520px; }
-.emoji-1F4D7 { background-position: -620px -540px; }
-.emoji-1F4D8 { background-position: -620px -560px; }
-.emoji-1F4D9 { background-position: -620px -580px; }
-.emoji-1F4DA { background-position: -620px -600px; }
-.emoji-1F4DB { background-position: 0 -620px; }
-.emoji-1F4DC { background-position: -20px -620px; }
-.emoji-1F4DD { background-position: -40px -620px; }
-.emoji-1F4DE { background-position: -60px -620px; }
-.emoji-1F4DF { background-position: -80px -620px; }
-.emoji-1F4E0 { background-position: -100px -620px; }
-.emoji-1F4E1 { background-position: -120px -620px; }
-.emoji-1F4E2 { background-position: -140px -620px; }
-.emoji-1F4E3 { background-position: -160px -620px; }
-.emoji-1F4E4 { background-position: -180px -620px; }
-.emoji-1F4E5 { background-position: -200px -620px; }
-.emoji-1F4E6 { background-position: -220px -620px; }
-.emoji-1F4E7 { background-position: -240px -620px; }
-.emoji-1F4E8 { background-position: -260px -620px; }
-.emoji-1F4E9 { background-position: -280px -620px; }
-.emoji-1F4EA { background-position: -300px -620px; }
-.emoji-1F4EB { background-position: -320px -620px; }
-.emoji-1F4EC { background-position: -340px -620px; }
-.emoji-1F4ED { background-position: -360px -620px; }
-.emoji-1F4EE { background-position: -380px -620px; }
-.emoji-1F4EF { background-position: -400px -620px; }
-.emoji-1F4F0 { background-position: -420px -620px; }
-.emoji-1F4F1 { background-position: -440px -620px; }
-.emoji-1F4F2 { background-position: -460px -620px; }
-.emoji-1F4F3 { background-position: -480px -620px; }
-.emoji-1F4F4 { background-position: -500px -620px; }
-.emoji-1F4F5 { background-position: -520px -620px; }
-.emoji-1F4F6 { background-position: -540px -620px; }
-.emoji-1F4F7 { background-position: -560px -620px; }
-.emoji-1F4F8 { background-position: -580px -620px; }
-.emoji-1F4F9 { background-position: -600px -620px; }
-.emoji-1F4FA { background-position: -620px -620px; }
-.emoji-1F4FB { background-position: -640px 0; }
-.emoji-1F4FC { background-position: -640px -20px; }
-.emoji-1F4FD { background-position: -640px -40px; }
-.emoji-1F4FF { background-position: -640px -60px; }
-.emoji-1F500 { background-position: -640px -80px; }
-.emoji-1F501 { background-position: -640px -100px; }
-.emoji-1F502 { background-position: -640px -120px; }
-.emoji-1F503 { background-position: -640px -140px; }
-.emoji-1F504 { background-position: -640px -160px; }
-.emoji-1F505 { background-position: -640px -180px; }
-.emoji-1F506 { background-position: -640px -200px; }
-.emoji-1F507 { background-position: -640px -220px; }
-.emoji-1F508 { background-position: -640px -240px; }
-.emoji-1F509 { background-position: -640px -260px; }
-.emoji-1F50A { background-position: -640px -280px; }
-.emoji-1F50B { background-position: -640px -300px; }
-.emoji-1F50C { background-position: -640px -320px; }
-.emoji-1F50D { background-position: -640px -340px; }
-.emoji-1F50E { background-position: -640px -360px; }
-.emoji-1F50F { background-position: -640px -380px; }
-.emoji-1F510 { background-position: -640px -400px; }
-.emoji-1F511 { background-position: -640px -420px; }
-.emoji-1F512 { background-position: -640px -440px; }
-.emoji-1F513 { background-position: -640px -460px; }
-.emoji-1F514 { background-position: -640px -480px; }
-.emoji-1F515 { background-position: -640px -500px; }
-.emoji-1F516 { background-position: -640px -520px; }
-.emoji-1F517 { background-position: -640px -540px; }
-.emoji-1F518 { background-position: -640px -560px; }
-.emoji-1F519 { background-position: -640px -580px; }
-.emoji-1F51A { background-position: -640px -600px; }
-.emoji-1F51B { background-position: -640px -620px; }
-.emoji-1F51C { background-position: 0 -640px; }
-.emoji-1F51D { background-position: -20px -640px; }
-.emoji-1F51E { background-position: -40px -640px; }
-.emoji-1F51F { background-position: -60px -640px; }
-.emoji-1F520 { background-position: -80px -640px; }
-.emoji-1F521 { background-position: -100px -640px; }
-.emoji-1F522 { background-position: -120px -640px; }
-.emoji-1F523 { background-position: -140px -640px; }
-.emoji-1F524 { background-position: -160px -640px; }
-.emoji-1F525 { background-position: -180px -640px; }
-.emoji-1F526 { background-position: -200px -640px; }
-.emoji-1F527 { background-position: -220px -640px; }
-.emoji-1F528 { background-position: -240px -640px; }
-.emoji-1F529 { background-position: -260px -640px; }
-.emoji-1F52A { background-position: -280px -640px; }
-.emoji-1F52B { background-position: -300px -640px; }
-.emoji-1F52C { background-position: -320px -640px; }
-.emoji-1F52D { background-position: -340px -640px; }
-.emoji-1F52E { background-position: -360px -640px; }
-.emoji-1F52F { background-position: -380px -640px; }
-.emoji-1F530 { background-position: -400px -640px; }
-.emoji-1F531 { background-position: -420px -640px; }
-.emoji-1F532 { background-position: -440px -640px; }
-.emoji-1F533 { background-position: -460px -640px; }
-.emoji-1F534 { background-position: -480px -640px; }
-.emoji-1F535 { background-position: -500px -640px; }
-.emoji-1F536 { background-position: -520px -640px; }
-.emoji-1F537 { background-position: -540px -640px; }
-.emoji-1F538 { background-position: -560px -640px; }
-.emoji-1F539 { background-position: -580px -640px; }
-.emoji-1F53A { background-position: -600px -640px; }
-.emoji-1F53B { background-position: -620px -640px; }
-.emoji-1F53C { background-position: -640px -640px; }
-.emoji-1F53D { background-position: -660px 0; }
-.emoji-1F549 { background-position: -660px -20px; }
-.emoji-1F54A { background-position: -660px -40px; }
-.emoji-1F54B { background-position: -660px -60px; }
-.emoji-1F54C { background-position: -660px -80px; }
-.emoji-1F54D { background-position: -660px -100px; }
-.emoji-1F54E { background-position: -660px -120px; }
-.emoji-1F550 { background-position: -660px -140px; }
-.emoji-1F551 { background-position: -660px -160px; }
-.emoji-1F552 { background-position: -660px -180px; }
-.emoji-1F553 { background-position: -660px -200px; }
-.emoji-1F554 { background-position: -660px -220px; }
-.emoji-1F555 { background-position: -660px -240px; }
-.emoji-1F556 { background-position: -660px -260px; }
-.emoji-1F557 { background-position: -660px -280px; }
-.emoji-1F558 { background-position: -660px -300px; }
-.emoji-1F559 { background-position: -660px -320px; }
-.emoji-1F55A { background-position: -660px -340px; }
-.emoji-1F55B { background-position: -660px -360px; }
-.emoji-1F55C { background-position: -660px -380px; }
-.emoji-1F55D { background-position: -660px -400px; }
-.emoji-1F55E { background-position: -660px -420px; }
-.emoji-1F55F { background-position: -660px -440px; }
-.emoji-1F560 { background-position: -660px -460px; }
-.emoji-1F561 { background-position: -660px -480px; }
-.emoji-1F562 { background-position: -660px -500px; }
-.emoji-1F563 { background-position: -660px -520px; }
-.emoji-1F564 { background-position: -660px -540px; }
-.emoji-1F565 { background-position: -660px -560px; }
-.emoji-1F566 { background-position: -660px -580px; }
-.emoji-1F567 { background-position: -660px -600px; }
-.emoji-1F56F { background-position: -660px -620px; }
-.emoji-1F570 { background-position: -660px -640px; }
-.emoji-1F573 { background-position: 0 -660px; }
-.emoji-1F574 { background-position: -20px -660px; }
-.emoji-1F575 { background-position: -40px -660px; }
-.emoji-1F575-1F3FB { background-position: -60px -660px; }
-.emoji-1F575-1F3FC { background-position: -80px -660px; }
-.emoji-1F575-1F3FD { background-position: -100px -660px; }
-.emoji-1F575-1F3FE { background-position: -120px -660px; }
-.emoji-1F575-1F3FF { background-position: -140px -660px; }
-.emoji-1F576 { background-position: -160px -660px; }
-.emoji-1F577 { background-position: -180px -660px; }
-.emoji-1F578 { background-position: -200px -660px; }
-.emoji-1F579 { background-position: -220px -660px; }
-.emoji-1F57A { background-position: -240px -660px; }
-.emoji-1F57A-1F3FB { background-position: -260px -660px; }
-.emoji-1F57A-1F3FC { background-position: -280px -660px; }
-.emoji-1F57A-1F3FD { background-position: -300px -660px; }
-.emoji-1F57A-1F3FE { background-position: -320px -660px; }
-.emoji-1F57A-1F3FF { background-position: -340px -660px; }
-.emoji-1F587 { background-position: -360px -660px; }
-.emoji-1F58A { background-position: -380px -660px; }
-.emoji-1F58B { background-position: -400px -660px; }
-.emoji-1F58C { background-position: -420px -660px; }
-.emoji-1F58D { background-position: -440px -660px; }
-.emoji-1F590 { background-position: -460px -660px; }
-.emoji-1F590-1F3FB { background-position: -480px -660px; }
-.emoji-1F590-1F3FC { background-position: -500px -660px; }
-.emoji-1F590-1F3FD { background-position: -520px -660px; }
-.emoji-1F590-1F3FE { background-position: -540px -660px; }
-.emoji-1F590-1F3FF { background-position: -560px -660px; }
-.emoji-1F595 { background-position: -580px -660px; }
-.emoji-1F595-1F3FB { background-position: -600px -660px; }
-.emoji-1F595-1F3FC { background-position: -620px -660px; }
-.emoji-1F595-1F3FD { background-position: -640px -660px; }
-.emoji-1F595-1F3FE { background-position: -660px -660px; }
-.emoji-1F595-1F3FF { background-position: -680px 0; }
-.emoji-1F596 { background-position: -680px -20px; }
-.emoji-1F596-1F3FB { background-position: -680px -40px; }
-.emoji-1F596-1F3FC { background-position: -680px -60px; }
-.emoji-1F596-1F3FD { background-position: -680px -80px; }
-.emoji-1F596-1F3FE { background-position: -680px -100px; }
-.emoji-1F596-1F3FF { background-position: -680px -120px; }
-.emoji-1F5A4 { background-position: -680px -140px; }
-.emoji-1F5A5 { background-position: -680px -160px; }
-.emoji-1F5A8 { background-position: -680px -180px; }
-.emoji-1F5B1 { background-position: -680px -200px; }
-.emoji-1F5B2 { background-position: -680px -220px; }
-.emoji-1F5BC { background-position: -680px -240px; }
-.emoji-1F5C2 { background-position: -680px -260px; }
-.emoji-1F5C3 { background-position: -680px -280px; }
-.emoji-1F5C4 { background-position: -680px -300px; }
-.emoji-1F5D1 { background-position: -680px -320px; }
-.emoji-1F5D2 { background-position: -680px -340px; }
-.emoji-1F5D3 { background-position: -680px -360px; }
-.emoji-1F5DC { background-position: -680px -380px; }
-.emoji-1F5DD { background-position: -680px -400px; }
-.emoji-1F5DE { background-position: -680px -420px; }
-.emoji-1F5E1 { background-position: -680px -440px; }
-.emoji-1F5E3 { background-position: -680px -460px; }
-.emoji-1F5EF { background-position: -680px -480px; }
-.emoji-1F5F3 { background-position: -680px -500px; }
-.emoji-1F5FA { background-position: -680px -520px; }
-.emoji-1F5FB { background-position: -680px -540px; }
-.emoji-1F5FC { background-position: -680px -560px; }
-.emoji-1F5FD { background-position: -680px -580px; }
-.emoji-1F5FE { background-position: -680px -600px; }
-.emoji-1F5FF { background-position: -680px -620px; }
-.emoji-1F600 { background-position: -680px -640px; }
-.emoji-1F601 { background-position: -680px -660px; }
-.emoji-1F602 { background-position: 0 -680px; }
-.emoji-1F603 { background-position: -20px -680px; }
-.emoji-1F604 { background-position: -40px -680px; }
-.emoji-1F605 { background-position: -60px -680px; }
-.emoji-1F606 { background-position: -80px -680px; }
-.emoji-1F607 { background-position: -100px -680px; }
-.emoji-1F608 { background-position: -120px -680px; }
-.emoji-1F609 { background-position: -140px -680px; }
-.emoji-1F60A { background-position: -160px -680px; }
-.emoji-1F60B { background-position: -180px -680px; }
-.emoji-1F60C { background-position: -200px -680px; }
-.emoji-1F60D { background-position: -220px -680px; }
-.emoji-1F60E { background-position: -240px -680px; }
-.emoji-1F60F { background-position: -260px -680px; }
-.emoji-1F610 { background-position: -280px -680px; }
-.emoji-1F611 { background-position: -300px -680px; }
-.emoji-1F612 { background-position: -320px -680px; }
-.emoji-1F613 { background-position: -340px -680px; }
-.emoji-1F614 { background-position: -360px -680px; }
-.emoji-1F615 { background-position: -380px -680px; }
-.emoji-1F616 { background-position: -400px -680px; }
-.emoji-1F617 { background-position: -420px -680px; }
-.emoji-1F618 { background-position: -440px -680px; }
-.emoji-1F619 { background-position: -460px -680px; }
-.emoji-1F61A { background-position: -480px -680px; }
-.emoji-1F61B { background-position: -500px -680px; }
-.emoji-1F61C { background-position: -520px -680px; }
-.emoji-1F61D { background-position: -540px -680px; }
-.emoji-1F61E { background-position: -560px -680px; }
-.emoji-1F61F { background-position: -580px -680px; }
-.emoji-1F620 { background-position: -600px -680px; }
-.emoji-1F621 { background-position: -620px -680px; }
-.emoji-1F622 { background-position: -640px -680px; }
-.emoji-1F623 { background-position: -660px -680px; }
-.emoji-1F624 { background-position: -680px -680px; }
-.emoji-1F625 { background-position: -700px 0; }
-.emoji-1F626 { background-position: -700px -20px; }
-.emoji-1F627 { background-position: -700px -40px; }
-.emoji-1F628 { background-position: -700px -60px; }
-.emoji-1F629 { background-position: -700px -80px; }
-.emoji-1F62A { background-position: -700px -100px; }
-.emoji-1F62B { background-position: -700px -120px; }
-.emoji-1F62C { background-position: -700px -140px; }
-.emoji-1F62D { background-position: -700px -160px; }
-.emoji-1F62E { background-position: -700px -180px; }
-.emoji-1F62F { background-position: -700px -200px; }
-.emoji-1F630 { background-position: -700px -220px; }
-.emoji-1F631 { background-position: -700px -240px; }
-.emoji-1F632 { background-position: -700px -260px; }
-.emoji-1F633 { background-position: -700px -280px; }
-.emoji-1F634 { background-position: -700px -300px; }
-.emoji-1F635 { background-position: -700px -320px; }
-.emoji-1F636 { background-position: -700px -340px; }
-.emoji-1F637 { background-position: -700px -360px; }
-.emoji-1F638 { background-position: -700px -380px; }
-.emoji-1F639 { background-position: -700px -400px; }
-.emoji-1F63A { background-position: -700px -420px; }
-.emoji-1F63B { background-position: -700px -440px; }
-.emoji-1F63C { background-position: -700px -460px; }
-.emoji-1F63D { background-position: -700px -480px; }
-.emoji-1F63E { background-position: -700px -500px; }
-.emoji-1F63F { background-position: -700px -520px; }
-.emoji-1F640 { background-position: -700px -540px; }
-.emoji-1F641 { background-position: -700px -560px; }
-.emoji-1F642 { background-position: -700px -580px; }
-.emoji-1F643 { background-position: -700px -600px; }
-.emoji-1F644 { background-position: -700px -620px; }
-.emoji-1F645 { background-position: -700px -640px; }
-.emoji-1F645-1F3FB { background-position: -700px -660px; }
-.emoji-1F645-1F3FC { background-position: -700px -680px; }
-.emoji-1F645-1F3FD { background-position: 0 -700px; }
-.emoji-1F645-1F3FE { background-position: -20px -700px; }
-.emoji-1F645-1F3FF { background-position: -40px -700px; }
-.emoji-1F646 { background-position: -60px -700px; }
-.emoji-1F646-1F3FB { background-position: -80px -700px; }
-.emoji-1F646-1F3FC { background-position: -100px -700px; }
-.emoji-1F646-1F3FD { background-position: -120px -700px; }
-.emoji-1F646-1F3FE { background-position: -140px -700px; }
-.emoji-1F646-1F3FF { background-position: -160px -700px; }
-.emoji-1F647 { background-position: -180px -700px; }
-.emoji-1F647-1F3FB { background-position: -200px -700px; }
-.emoji-1F647-1F3FC { background-position: -220px -700px; }
-.emoji-1F647-1F3FD { background-position: -240px -700px; }
-.emoji-1F647-1F3FE { background-position: -260px -700px; }
-.emoji-1F647-1F3FF { background-position: -280px -700px; }
-.emoji-1F648 { background-position: -300px -700px; }
-.emoji-1F649 { background-position: -320px -700px; }
-.emoji-1F64A { background-position: -340px -700px; }
-.emoji-1F64B { background-position: -360px -700px; }
-.emoji-1F64B-1F3FB { background-position: -380px -700px; }
-.emoji-1F64B-1F3FC { background-position: -400px -700px; }
-.emoji-1F64B-1F3FD { background-position: -420px -700px; }
-.emoji-1F64B-1F3FE { background-position: -440px -700px; }
-.emoji-1F64B-1F3FF { background-position: -460px -700px; }
-.emoji-1F64C { background-position: -480px -700px; }
-.emoji-1F64C-1F3FB { background-position: -500px -700px; }
-.emoji-1F64C-1F3FC { background-position: -520px -700px; }
-.emoji-1F64C-1F3FD { background-position: -540px -700px; }
-.emoji-1F64C-1F3FE { background-position: -560px -700px; }
-.emoji-1F64C-1F3FF { background-position: -580px -700px; }
-.emoji-1F64D { background-position: -600px -700px; }
-.emoji-1F64D-1F3FB { background-position: -620px -700px; }
-.emoji-1F64D-1F3FC { background-position: -640px -700px; }
-.emoji-1F64D-1F3FD { background-position: -660px -700px; }
-.emoji-1F64D-1F3FE { background-position: -680px -700px; }
-.emoji-1F64D-1F3FF { background-position: -700px -700px; }
-.emoji-1F64E { background-position: -720px 0; }
-.emoji-1F64E-1F3FB { background-position: -720px -20px; }
-.emoji-1F64E-1F3FC { background-position: -720px -40px; }
-.emoji-1F64E-1F3FD { background-position: -720px -60px; }
-.emoji-1F64E-1F3FE { background-position: -720px -80px; }
-.emoji-1F64E-1F3FF { background-position: -720px -100px; }
-.emoji-1F64F { background-position: -720px -120px; }
-.emoji-1F64F-1F3FB { background-position: -720px -140px; }
-.emoji-1F64F-1F3FC { background-position: -720px -160px; }
-.emoji-1F64F-1F3FD { background-position: -720px -180px; }
-.emoji-1F64F-1F3FE { background-position: -720px -200px; }
-.emoji-1F64F-1F3FF { background-position: -720px -220px; }
-.emoji-1F680 { background-position: -720px -240px; }
-.emoji-1F681 { background-position: -720px -260px; }
-.emoji-1F682 { background-position: -720px -280px; }
-.emoji-1F683 { background-position: -720px -300px; }
-.emoji-1F684 { background-position: -720px -320px; }
-.emoji-1F685 { background-position: -720px -340px; }
-.emoji-1F686 { background-position: -720px -360px; }
-.emoji-1F687 { background-position: -720px -380px; }
-.emoji-1F688 { background-position: -720px -400px; }
-.emoji-1F689 { background-position: -720px -420px; }
-.emoji-1F68A { background-position: -720px -440px; }
-.emoji-1F68B { background-position: -720px -460px; }
-.emoji-1F68C { background-position: -720px -480px; }
-.emoji-1F68D { background-position: -720px -500px; }
-.emoji-1F68E { background-position: -720px -520px; }
-.emoji-1F68F { background-position: -720px -540px; }
-.emoji-1F690 { background-position: -720px -560px; }
-.emoji-1F691 { background-position: -720px -580px; }
-.emoji-1F692 { background-position: -720px -600px; }
-.emoji-1F693 { background-position: -720px -620px; }
-.emoji-1F694 { background-position: -720px -640px; }
-.emoji-1F695 { background-position: -720px -660px; }
-.emoji-1F696 { background-position: -720px -680px; }
-.emoji-1F697 { background-position: -720px -700px; }
-.emoji-1F698 { background-position: 0 -720px; }
-.emoji-1F699 { background-position: -20px -720px; }
-.emoji-1F69A { background-position: -40px -720px; }
-.emoji-1F69B { background-position: -60px -720px; }
-.emoji-1F69C { background-position: -80px -720px; }
-.emoji-1F69D { background-position: -100px -720px; }
-.emoji-1F69E { background-position: -120px -720px; }
-.emoji-1F69F { background-position: -140px -720px; }
-.emoji-1F6A0 { background-position: -160px -720px; }
-.emoji-1F6A1 { background-position: -180px -720px; }
-.emoji-1F6A2 { background-position: -200px -720px; }
-.emoji-1F6A3 { background-position: -220px -720px; }
-.emoji-1F6A3-1F3FB { background-position: -240px -720px; }
-.emoji-1F6A3-1F3FC { background-position: -260px -720px; }
-.emoji-1F6A3-1F3FD { background-position: -280px -720px; }
-.emoji-1F6A3-1F3FE { background-position: -300px -720px; }
-.emoji-1F6A3-1F3FF { background-position: -320px -720px; }
-.emoji-1F6A4 { background-position: -340px -720px; }
-.emoji-1F6A5 { background-position: -360px -720px; }
-.emoji-1F6A6 { background-position: -380px -720px; }
-.emoji-1F6A7 { background-position: -400px -720px; }
-.emoji-1F6A8 { background-position: -420px -720px; }
-.emoji-1F6A9 { background-position: -440px -720px; }
-.emoji-1F6AA { background-position: -460px -720px; }
-.emoji-1F6AB { background-position: -480px -720px; }
-.emoji-1F6AC { background-position: -500px -720px; }
-.emoji-1F6AD { background-position: -520px -720px; }
-.emoji-1F6AE { background-position: -540px -720px; }
-.emoji-1F6AF { background-position: -560px -720px; }
-.emoji-1F6B0 { background-position: -580px -720px; }
-.emoji-1F6B1 { background-position: -600px -720px; }
-.emoji-1F6B2 { background-position: -620px -720px; }
-.emoji-1F6B3 { background-position: -640px -720px; }
-.emoji-1F6B4 { background-position: -660px -720px; }
-.emoji-1F6B4-1F3FB { background-position: -680px -720px; }
-.emoji-1F6B4-1F3FC { background-position: -700px -720px; }
-.emoji-1F6B4-1F3FD { background-position: -720px -720px; }
-.emoji-1F6B4-1F3FE { background-position: -740px 0; }
-.emoji-1F6B4-1F3FF { background-position: -740px -20px; }
-.emoji-1F6B5 { background-position: -740px -40px; }
-.emoji-1F6B5-1F3FB { background-position: -740px -60px; }
-.emoji-1F6B5-1F3FC { background-position: -740px -80px; }
-.emoji-1F6B5-1F3FD { background-position: -740px -100px; }
-.emoji-1F6B5-1F3FE { background-position: -740px -120px; }
-.emoji-1F6B5-1F3FF { background-position: -740px -140px; }
-.emoji-1F6B6 { background-position: -740px -160px; }
-.emoji-1F6B6-1F3FB { background-position: -740px -180px; }
-.emoji-1F6B6-1F3FC { background-position: -740px -200px; }
-.emoji-1F6B6-1F3FD { background-position: -740px -220px; }
-.emoji-1F6B6-1F3FE { background-position: -740px -240px; }
-.emoji-1F6B6-1F3FF { background-position: -740px -260px; }
-.emoji-1F6B7 { background-position: -740px -280px; }
-.emoji-1F6B8 { background-position: -740px -300px; }
-.emoji-1F6B9 { background-position: -740px -320px; }
-.emoji-1F6BA { background-position: -740px -340px; }
-.emoji-1F6BB { background-position: -740px -360px; }
-.emoji-1F6BC { background-position: -740px -380px; }
-.emoji-1F6BD { background-position: -740px -400px; }
-.emoji-1F6BE { background-position: -740px -420px; }
-.emoji-1F6BF { background-position: -740px -440px; }
-.emoji-1F6C0 { background-position: -740px -460px; }
-.emoji-1F6C0-1F3FB { background-position: -740px -480px; }
-.emoji-1F6C0-1F3FC { background-position: -740px -500px; }
-.emoji-1F6C0-1F3FD { background-position: -740px -520px; }
-.emoji-1F6C0-1F3FE { background-position: -740px -540px; }
-.emoji-1F6C0-1F3FF { background-position: -740px -560px; }
-.emoji-1F6C1 { background-position: -740px -580px; }
-.emoji-1F6C2 { background-position: -740px -600px; }
-.emoji-1F6C3 { background-position: -740px -620px; }
-.emoji-1F6C4 { background-position: -740px -640px; }
-.emoji-1F6C5 { background-position: -740px -660px; }
-.emoji-1F6CB { background-position: -740px -680px; }
-.emoji-1F6CC { background-position: -740px -700px; }
-.emoji-1F6CD { background-position: -740px -720px; }
-.emoji-1F6CE { background-position: 0 -740px; }
-.emoji-1F6CF { background-position: -20px -740px; }
-.emoji-1F6D0 { background-position: -40px -740px; }
-.emoji-1F6D1 { background-position: -60px -740px; }
-.emoji-1F6D2 { background-position: -80px -740px; }
-.emoji-1F6E0 { background-position: -100px -740px; }
-.emoji-1F6E1 { background-position: -120px -740px; }
-.emoji-1F6E2 { background-position: -140px -740px; }
-.emoji-1F6E3 { background-position: -160px -740px; }
-.emoji-1F6E4 { background-position: -180px -740px; }
-.emoji-1F6E5 { background-position: -200px -740px; }
-.emoji-1F6E9 { background-position: -220px -740px; }
-.emoji-1F6EB { background-position: -240px -740px; }
-.emoji-1F6EC { background-position: -260px -740px; }
-.emoji-1F6F0 { background-position: -280px -740px; }
-.emoji-1F6F3 { background-position: -300px -740px; }
-.emoji-1F6F4 { background-position: -320px -740px; }
-.emoji-1F6F5 { background-position: -340px -740px; }
-.emoji-1F6F6 { background-position: -360px -740px; }
-.emoji-1F910 { background-position: -380px -740px; }
-.emoji-1F911 { background-position: -400px -740px; }
-.emoji-1F912 { background-position: -420px -740px; }
-.emoji-1F913 { background-position: -440px -740px; }
-.emoji-1F914 { background-position: -460px -740px; }
-.emoji-1F915 { background-position: -480px -740px; }
-.emoji-1F916 { background-position: -500px -740px; }
-.emoji-1F917 { background-position: -520px -740px; }
-.emoji-1F918 { background-position: -540px -740px; }
-.emoji-1F918-1F3FB { background-position: -560px -740px; }
-.emoji-1F918-1F3FC { background-position: -580px -740px; }
-.emoji-1F918-1F3FD { background-position: -600px -740px; }
-.emoji-1F918-1F3FE { background-position: -620px -740px; }
-.emoji-1F918-1F3FF { background-position: -640px -740px; }
-.emoji-1F919 { background-position: -660px -740px; }
-.emoji-1F919-1F3FB { background-position: -680px -740px; }
-.emoji-1F919-1F3FC { background-position: -700px -740px; }
-.emoji-1F919-1F3FD { background-position: -720px -740px; }
-.emoji-1F919-1F3FE { background-position: -740px -740px; }
-.emoji-1F919-1F3FF { background-position: -760px 0; }
-.emoji-1F91A { background-position: -760px -20px; }
-.emoji-1F91A-1F3FB { background-position: -760px -40px; }
-.emoji-1F91A-1F3FC { background-position: -760px -60px; }
-.emoji-1F91A-1F3FD { background-position: -760px -80px; }
-.emoji-1F91A-1F3FE { background-position: -760px -100px; }
-.emoji-1F91A-1F3FF { background-position: -760px -120px; }
-.emoji-1F91B { background-position: -760px -140px; }
-.emoji-1F91B-1F3FB { background-position: -760px -160px; }
-.emoji-1F91B-1F3FC { background-position: -760px -180px; }
-.emoji-1F91B-1F3FD { background-position: -760px -200px; }
-.emoji-1F91B-1F3FE { background-position: -760px -220px; }
-.emoji-1F91B-1F3FF { background-position: -760px -240px; }
-.emoji-1F91C { background-position: -760px -260px; }
-.emoji-1F91C-1F3FB { background-position: -760px -280px; }
-.emoji-1F91C-1F3FC { background-position: -760px -300px; }
-.emoji-1F91C-1F3FD { background-position: -760px -320px; }
-.emoji-1F91C-1F3FE { background-position: -760px -340px; }
-.emoji-1F91C-1F3FF { background-position: -760px -360px; }
-.emoji-1F91D { background-position: -760px -380px; }
-.emoji-1F91D-1F3FB { background-position: -760px -400px; }
-.emoji-1F91D-1F3FC { background-position: -760px -420px; }
-.emoji-1F91D-1F3FD { background-position: -760px -440px; }
-.emoji-1F91D-1F3FE { background-position: -760px -460px; }
-.emoji-1F91D-1F3FF { background-position: -760px -480px; }
-.emoji-1F91E { background-position: -760px -500px; }
-.emoji-1F91E-1F3FB { background-position: -760px -520px; }
-.emoji-1F91E-1F3FC { background-position: -760px -540px; }
-.emoji-1F91E-1F3FD { background-position: -760px -560px; }
-.emoji-1F91E-1F3FE { background-position: -760px -580px; }
-.emoji-1F91E-1F3FF { background-position: -760px -600px; }
-.emoji-1F920 { background-position: -760px -620px; }
-.emoji-1F921 { background-position: -760px -640px; }
-.emoji-1F922 { background-position: -760px -660px; }
-.emoji-1F923 { background-position: -760px -680px; }
-.emoji-1F924 { background-position: -760px -700px; }
-.emoji-1F925 { background-position: -760px -720px; }
-.emoji-1F926 { background-position: -760px -740px; }
-.emoji-1F926-1F3FB { background-position: 0 -760px; }
-.emoji-1F926-1F3FC { background-position: -20px -760px; }
-.emoji-1F926-1F3FD { background-position: -40px -760px; }
-.emoji-1F926-1F3FE { background-position: -60px -760px; }
-.emoji-1F926-1F3FF { background-position: -80px -760px; }
-.emoji-1F927 { background-position: -100px -760px; }
-.emoji-1F930 { background-position: -120px -760px; }
-.emoji-1F930-1F3FB { background-position: -140px -760px; }
-.emoji-1F930-1F3FC { background-position: -160px -760px; }
-.emoji-1F930-1F3FD { background-position: -180px -760px; }
-.emoji-1F930-1F3FE { background-position: -200px -760px; }
-.emoji-1F930-1F3FF { background-position: -220px -760px; }
-.emoji-1F933 { background-position: -240px -760px; }
-.emoji-1F933-1F3FB { background-position: -260px -760px; }
-.emoji-1F933-1F3FC { background-position: -280px -760px; }
-.emoji-1F933-1F3FD { background-position: -300px -760px; }
-.emoji-1F933-1F3FE { background-position: -320px -760px; }
-.emoji-1F933-1F3FF { background-position: -340px -760px; }
-.emoji-1F934 { background-position: -360px -760px; }
-.emoji-1F934-1F3FB { background-position: -380px -760px; }
-.emoji-1F934-1F3FC { background-position: -400px -760px; }
-.emoji-1F934-1F3FD { background-position: -420px -760px; }
-.emoji-1F934-1F3FE { background-position: -440px -760px; }
-.emoji-1F934-1F3FF { background-position: -460px -760px; }
-.emoji-1F935 { background-position: -480px -760px; }
-.emoji-1F935-1F3FB { background-position: -500px -760px; }
-.emoji-1F935-1F3FC { background-position: -520px -760px; }
-.emoji-1F935-1F3FD { background-position: -540px -760px; }
-.emoji-1F935-1F3FE { background-position: -560px -760px; }
-.emoji-1F935-1F3FF { background-position: -580px -760px; }
-.emoji-1F936 { background-position: -600px -760px; }
-.emoji-1F936-1F3FB { background-position: -620px -760px; }
-.emoji-1F936-1F3FC { background-position: -640px -760px; }
-.emoji-1F936-1F3FD { background-position: -660px -760px; }
-.emoji-1F936-1F3FE { background-position: -680px -760px; }
-.emoji-1F936-1F3FF { background-position: -700px -760px; }
-.emoji-1F937 { background-position: -720px -760px; }
-.emoji-1F937-1F3FB { background-position: -740px -760px; }
-.emoji-1F937-1F3FC { background-position: -760px -760px; }
-.emoji-1F937-1F3FD { background-position: -780px 0; }
-.emoji-1F937-1F3FE { background-position: -780px -20px; }
-.emoji-1F937-1F3FF { background-position: -780px -40px; }
-.emoji-1F938 { background-position: -780px -60px; }
-.emoji-1F938-1F3FB { background-position: -780px -80px; }
-.emoji-1F938-1F3FC { background-position: -780px -100px; }
-.emoji-1F938-1F3FD { background-position: -780px -120px; }
-.emoji-1F938-1F3FE { background-position: -780px -140px; }
-.emoji-1F938-1F3FF { background-position: -780px -160px; }
-.emoji-1F939 { background-position: -780px -180px; }
-.emoji-1F939-1F3FB { background-position: -780px -200px; }
-.emoji-1F939-1F3FC { background-position: -780px -220px; }
-.emoji-1F939-1F3FD { background-position: -780px -240px; }
-.emoji-1F939-1F3FE { background-position: -780px -260px; }
-.emoji-1F939-1F3FF { background-position: -780px -280px; }
-.emoji-1F93A { background-position: -780px -300px; }
-.emoji-1F93C { background-position: -780px -320px; }
-.emoji-1F93C-1F3FB { background-position: -780px -340px; }
-.emoji-1F93C-1F3FC { background-position: -780px -360px; }
-.emoji-1F93C-1F3FD { background-position: -780px -380px; }
-.emoji-1F93C-1F3FE { background-position: -780px -400px; }
-.emoji-1F93C-1F3FF { background-position: -780px -420px; }
-.emoji-1F93D { background-position: -780px -440px; }
-.emoji-1F93D-1F3FB { background-position: -780px -460px; }
-.emoji-1F93D-1F3FC { background-position: -780px -480px; }
-.emoji-1F93D-1F3FD { background-position: -780px -500px; }
-.emoji-1F93D-1F3FE { background-position: -780px -520px; }
-.emoji-1F93D-1F3FF { background-position: -780px -540px; }
-.emoji-1F93E { background-position: -780px -560px; }
-.emoji-1F93E-1F3FB { background-position: -780px -580px; }
-.emoji-1F93E-1F3FC { background-position: -780px -600px; }
-.emoji-1F93E-1F3FD { background-position: -780px -620px; }
-.emoji-1F93E-1F3FE { background-position: -780px -640px; }
-.emoji-1F93E-1F3FF { background-position: -780px -660px; }
-.emoji-1F940 { background-position: -780px -680px; }
-.emoji-1F941 { background-position: -780px -700px; }
-.emoji-1F942 { background-position: -780px -720px; }
-.emoji-1F943 { background-position: -780px -740px; }
-.emoji-1F944 { background-position: -780px -760px; }
-.emoji-1F945 { background-position: 0 -780px; }
-.emoji-1F947 { background-position: -20px -780px; }
-.emoji-1F948 { background-position: -40px -780px; }
-.emoji-1F949 { background-position: -60px -780px; }
-.emoji-1F94A { background-position: -80px -780px; }
-.emoji-1F94B { background-position: -100px -780px; }
-.emoji-1F950 { background-position: -120px -780px; }
-.emoji-1F951 { background-position: -140px -780px; }
-.emoji-1F952 { background-position: -160px -780px; }
-.emoji-1F953 { background-position: -180px -780px; }
-.emoji-1F954 { background-position: -200px -780px; }
-.emoji-1F955 { background-position: -220px -780px; }
-.emoji-1F956 { background-position: -240px -780px; }
-.emoji-1F957 { background-position: -260px -780px; }
-.emoji-1F958 { background-position: -280px -780px; }
-.emoji-1F959 { background-position: -300px -780px; }
-.emoji-1F95A { background-position: -320px -780px; }
-.emoji-1F95B { background-position: -340px -780px; }
-.emoji-1F95C { background-position: -360px -780px; }
-.emoji-1F95D { background-position: -380px -780px; }
-.emoji-1F95E { background-position: -400px -780px; }
-.emoji-1F980 { background-position: -420px -780px; }
-.emoji-1F981 { background-position: -440px -780px; }
-.emoji-1F982 { background-position: -460px -780px; }
-.emoji-1F983 { background-position: -480px -780px; }
-.emoji-1F984 { background-position: -500px -780px; }
-.emoji-1F985 { background-position: -520px -780px; }
-.emoji-1F986 { background-position: -540px -780px; }
-.emoji-1F987 { background-position: -560px -780px; }
-.emoji-1F988 { background-position: -580px -780px; }
-.emoji-1F989 { background-position: -600px -780px; }
-.emoji-1F98A { background-position: -620px -780px; }
-.emoji-1F98B { background-position: -640px -780px; }
-.emoji-1F98C { background-position: -660px -780px; }
-.emoji-1F98D { background-position: -680px -780px; }
-.emoji-1F98E { background-position: -700px -780px; }
-.emoji-1F98F { background-position: -720px -780px; }
-.emoji-1F990 { background-position: -740px -780px; }
-.emoji-1F991 { background-position: -760px -780px; }
-.emoji-1F9C0 { background-position: -780px -780px; }
-.emoji-203C { background-position: -800px 0; }
-.emoji-2049 { background-position: -800px -20px; }
-.emoji-2122 { background-position: -800px -40px; }
-.emoji-2139 { background-position: -800px -60px; }
-.emoji-2194 { background-position: -800px -80px; }
-.emoji-2195 { background-position: -800px -100px; }
-.emoji-2196 { background-position: -800px -120px; }
-.emoji-2197 { background-position: -800px -140px; }
-.emoji-2198 { background-position: -800px -160px; }
-.emoji-2199 { background-position: -800px -180px; }
-.emoji-21A9 { background-position: -800px -200px; }
-.emoji-21AA { background-position: -800px -220px; }
-.emoji-231A { background-position: -800px -240px; }
-.emoji-231B { background-position: -800px -260px; }
-.emoji-2328 { background-position: -800px -280px; }
-.emoji-23CF { background-position: -800px -300px; }
-.emoji-23E9 { background-position: -800px -320px; }
-.emoji-23EA { background-position: -800px -340px; }
-.emoji-23EB { background-position: -800px -360px; }
-.emoji-23EC { background-position: -800px -380px; }
-.emoji-23ED { background-position: -800px -400px; }
-.emoji-23EE { background-position: -800px -420px; }
-.emoji-23EF { background-position: -800px -440px; }
-.emoji-23F0 { background-position: -800px -460px; }
-.emoji-23F1 { background-position: -800px -480px; }
-.emoji-23F2 { background-position: -800px -500px; }
-.emoji-23F3 { background-position: -800px -520px; }
-.emoji-23F8 { background-position: -800px -540px; }
-.emoji-23F9 { background-position: -800px -560px; }
-.emoji-23FA { background-position: -800px -580px; }
-.emoji-24C2 { background-position: -800px -600px; }
-.emoji-25AA { background-position: -800px -620px; }
-.emoji-25AB { background-position: -800px -640px; }
-.emoji-25B6 { background-position: -800px -660px; }
-.emoji-25C0 { background-position: -800px -680px; }
-.emoji-25FB { background-position: -800px -700px; }
-.emoji-25FC { background-position: -800px -720px; }
-.emoji-25FD { background-position: -800px -740px; }
-.emoji-25FE { background-position: -800px -760px; }
-.emoji-2600 { background-position: -800px -780px; }
-.emoji-2601 { background-position: 0 -800px; }
-.emoji-2602 { background-position: -20px -800px; }
-.emoji-2603 { background-position: -40px -800px; }
-.emoji-2604 { background-position: -60px -800px; }
-.emoji-260E { background-position: -80px -800px; }
-.emoji-2611 { background-position: -100px -800px; }
-.emoji-2614 { background-position: -120px -800px; }
-.emoji-2615 { background-position: -140px -800px; }
-.emoji-2618 { background-position: -160px -800px; }
-.emoji-261D { background-position: -180px -800px; }
-.emoji-261D-1F3FB { background-position: -200px -800px; }
-.emoji-261D-1F3FC { background-position: -220px -800px; }
-.emoji-261D-1F3FD { background-position: -240px -800px; }
-.emoji-261D-1F3FE { background-position: -260px -800px; }
-.emoji-261D-1F3FF { background-position: -280px -800px; }
-.emoji-2620 { background-position: -300px -800px; }
-.emoji-2622 { background-position: -320px -800px; }
-.emoji-2623 { background-position: -340px -800px; }
-.emoji-2626 { background-position: -360px -800px; }
-.emoji-262A { background-position: -380px -800px; }
-.emoji-262E { background-position: -400px -800px; }
-.emoji-262F { background-position: -420px -800px; }
-.emoji-2638 { background-position: -440px -800px; }
-.emoji-2639 { background-position: -460px -800px; }
-.emoji-263A { background-position: -480px -800px; }
-.emoji-2648 { background-position: -500px -800px; }
-.emoji-2649 { background-position: -520px -800px; }
-.emoji-264A { background-position: -540px -800px; }
-.emoji-264B { background-position: -560px -800px; }
-.emoji-264C { background-position: -580px -800px; }
-.emoji-264D { background-position: -600px -800px; }
-.emoji-264E { background-position: -620px -800px; }
-.emoji-264F { background-position: -640px -800px; }
-.emoji-2650 { background-position: -660px -800px; }
-.emoji-2651 { background-position: -680px -800px; }
-.emoji-2652 { background-position: -700px -800px; }
-.emoji-2653 { background-position: -720px -800px; }
-.emoji-2660 { background-position: -740px -800px; }
-.emoji-2663 { background-position: -760px -800px; }
-.emoji-2665 { background-position: -780px -800px; }
-.emoji-2666 { background-position: -800px -800px; }
-.emoji-2668 { background-position: -820px 0; }
-.emoji-267B { background-position: -820px -20px; }
-.emoji-267F { background-position: -820px -40px; }
-.emoji-2692 { background-position: -820px -60px; }
-.emoji-2693 { background-position: -820px -80px; }
-.emoji-2694 { background-position: -820px -100px; }
-.emoji-2696 { background-position: -820px -120px; }
-.emoji-2697 { background-position: -820px -140px; }
-.emoji-2699 { background-position: -820px -160px; }
-.emoji-269B { background-position: -820px -180px; }
-.emoji-269C { background-position: -820px -200px; }
-.emoji-26A0 { background-position: -820px -220px; }
-.emoji-26A1 { background-position: -820px -240px; }
-.emoji-26AA { background-position: -820px -260px; }
-.emoji-26AB { background-position: -820px -280px; }
-.emoji-26B0 { background-position: -820px -300px; }
-.emoji-26B1 { background-position: -820px -320px; }
-.emoji-26BD { background-position: -820px -340px; }
-.emoji-26BE { background-position: -820px -360px; }
-.emoji-26C4 { background-position: -820px -380px; }
-.emoji-26C5 { background-position: -820px -400px; }
-.emoji-26C8 { background-position: -820px -420px; }
-.emoji-26CE { background-position: -820px -440px; }
-.emoji-26CF { background-position: -820px -460px; }
-.emoji-26D1 { background-position: -820px -480px; }
-.emoji-26D3 { background-position: -820px -500px; }
-.emoji-26D4 { background-position: -820px -520px; }
-.emoji-26E9 { background-position: -820px -540px; }
-.emoji-26EA { background-position: -820px -560px; }
-.emoji-26F0 { background-position: -820px -580px; }
-.emoji-26F1 { background-position: -820px -600px; }
-.emoji-26F2 { background-position: -820px -620px; }
-.emoji-26F3 { background-position: -820px -640px; }
-.emoji-26F4 { background-position: -820px -660px; }
-.emoji-26F5 { background-position: -820px -680px; }
-.emoji-26F7 { background-position: -820px -700px; }
-.emoji-26F8 { background-position: -820px -720px; }
-.emoji-26F9 { background-position: -820px -740px; }
-.emoji-26F9-1F3FB { background-position: -820px -760px; }
-.emoji-26F9-1F3FC { background-position: -820px -780px; }
-.emoji-26F9-1F3FD { background-position: -820px -800px; }
-.emoji-26F9-1F3FE { background-position: 0 -820px; }
-.emoji-26F9-1F3FF { background-position: -20px -820px; }
-.emoji-26FA { background-position: -40px -820px; }
-.emoji-26FD { background-position: -60px -820px; }
-.emoji-2702 { background-position: -80px -820px; }
-.emoji-2705 { background-position: -100px -820px; }
-.emoji-2708 { background-position: -120px -820px; }
-.emoji-2709 { background-position: -140px -820px; }
-.emoji-270A { background-position: -160px -820px; }
-.emoji-270A-1F3FB { background-position: -180px -820px; }
-.emoji-270A-1F3FC { background-position: -200px -820px; }
-.emoji-270A-1F3FD { background-position: -220px -820px; }
-.emoji-270A-1F3FE { background-position: -240px -820px; }
-.emoji-270A-1F3FF { background-position: -260px -820px; }
-.emoji-270B { background-position: -280px -820px; }
-.emoji-270B-1F3FB { background-position: -300px -820px; }
-.emoji-270B-1F3FC { background-position: -320px -820px; }
-.emoji-270B-1F3FD { background-position: -340px -820px; }
-.emoji-270B-1F3FE { background-position: -360px -820px; }
-.emoji-270B-1F3FF { background-position: -380px -820px; }
-.emoji-270C { background-position: -400px -820px; }
-.emoji-270C-1F3FB { background-position: -420px -820px; }
-.emoji-270C-1F3FC { background-position: -440px -820px; }
-.emoji-270C-1F3FD { background-position: -460px -820px; }
-.emoji-270C-1F3FE { background-position: -480px -820px; }
-.emoji-270C-1F3FF { background-position: -500px -820px; }
-.emoji-270D { background-position: -520px -820px; }
-.emoji-270D-1F3FB { background-position: -540px -820px; }
-.emoji-270D-1F3FC { background-position: -560px -820px; }
-.emoji-270D-1F3FD { background-position: -580px -820px; }
-.emoji-270D-1F3FE { background-position: -600px -820px; }
-.emoji-270D-1F3FF { background-position: -620px -820px; }
-.emoji-270F { background-position: -640px -820px; }
-.emoji-2712 { background-position: -660px -820px; }
-.emoji-2714 { background-position: -680px -820px; }
-.emoji-2716 { background-position: -700px -820px; }
-.emoji-271D { background-position: -720px -820px; }
-.emoji-2721 { background-position: -740px -820px; }
-.emoji-2728 { background-position: -760px -820px; }
-.emoji-2733 { background-position: -780px -820px; }
-.emoji-2734 { background-position: -800px -820px; }
-.emoji-2744 { background-position: -820px -820px; }
-.emoji-2747 { background-position: -840px 0; }
-.emoji-274C { background-position: -840px -20px; }
-.emoji-274E { background-position: -840px -40px; }
-.emoji-2753 { background-position: -840px -60px; }
-.emoji-2754 { background-position: -840px -80px; }
-.emoji-2755 { background-position: -840px -100px; }
-.emoji-2757 { background-position: -840px -120px; }
-.emoji-2763 { background-position: -840px -140px; }
-.emoji-2764 { background-position: -840px -160px; }
-.emoji-2795 { background-position: -840px -180px; }
-.emoji-2796 { background-position: -840px -200px; }
-.emoji-2797 { background-position: -840px -220px; }
-.emoji-27A1 { background-position: -840px -240px; }
-.emoji-27B0 { background-position: -840px -260px; }
-.emoji-27BF { background-position: -840px -280px; }
-.emoji-2934 { background-position: -840px -300px; }
-.emoji-2935 { background-position: -840px -320px; }
-.emoji-2B05 { background-position: -840px -340px; }
-.emoji-2B06 { background-position: -840px -360px; }
-.emoji-2B07 { background-position: -840px -380px; }
-.emoji-2B1B { background-position: -840px -400px; }
-.emoji-2B1C { background-position: -840px -420px; }
-.emoji-2B50 { background-position: -840px -440px; }
-.emoji-2B55 { background-position: -840px -460px; }
-.emoji-3030 { background-position: -840px -480px; }
-.emoji-303D { background-position: -840px -500px; }
-.emoji-3297 { background-position: -840px -520px; }
-.emoji-3299 { background-position: -840px -540px; }
-
-.emoji-icon {
-  background-image: image-url('emoji.png');
-  background-repeat: no-repeat;
-  height: 20px;
-  width: 20px;
-
-  @media only screen and (-webkit-min-device-pixel-ratio: 2),
-         only screen and (min--moz-device-pixel-ratio: 2),
-         only screen and (-o-min-device-pixel-ratio: 2/1),
-         only screen and (min-device-pixel-ratio: 2),
-         only screen and (min-resolution: 192dpi),
-         only screen and (min-resolution: 2dppx) {
-    background-image: image-url('emoji@2x.png');
-    background-size: 860px 840px;
-  }
+gl-emoji {
+  display: inline-block;
+  display: inline-flex;
+  vertical-align: middle;
+  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  font-size: 1.5em;
 }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 30f242a35db7103192dd7bf8209f0ef48712ba25..ffece53a0936e73a996abfbced13639d143073af 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -271,6 +271,7 @@ span.idiff {
       font-size: 13px;
       line-height: 28px;
       display: inline-block;
+      float: none;
     }
   }
 }
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index e3da467a27c92325772c6f7fc52795ff9e96a02b..51805c5d7340be3b85c023e5ff5439d1e16e896a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,10 +1,24 @@
 .filter-item {
-  margin-right: 6px;
   vertical-align: top;
 
   &.reset-filters {
     padding: 7px;
   }
+
+  &.update-issues-btn {
+    float: right;
+    margin-right: 0;
+
+    @media (max-width: $screen-xs-max) {
+      float: none;
+    }
+  }
+}
+
+.filters-section {
+  @media (max-width: $screen-xs-max) {
+    display: inline-block;
+  }
 }
 
 @media (min-width: $screen-sm-min) {
@@ -14,6 +28,20 @@
       width: 132px;
     }
   }
+
+  .filter-item:not(:last-child) {
+    margin-right: 6px;
+  }
+
+  .sort-filter {
+    display: inline-block;
+    float: right;
+  }
+
+  .dropdown-menu-sort {
+    left: auto;
+    right: 0;
+  }
 }
 
 @media (max-width: $screen-xs-max) {
@@ -21,11 +49,106 @@
     display: block;
     margin: 0 0 10px;
   }
+
+  .dropdown-menu-toggle,
+  .update-issues-btn .btn {
+    width: 100%;
+  }
 }
 
 .filtered-search-container {
   display: -webkit-flex;
   display: flex;
+
+  @media (max-width: $screen-xs-min) {
+    -webkit-flex-direction: column;
+    flex-direction: column;
+  }
+
+  .tokens-container {
+    display: -webkit-flex;
+    display: flex;
+    flex: 1;
+    -webkit-flex: 1;
+    padding-left: 30px;
+    position: relative;
+    margin-bottom: 0;
+  }
+
+  .input-token {
+    max-width: 200px;
+  }
+
+  .input-token:only-child,
+  .input-token:last-child {
+    flex: 1;
+    -webkit-flex: 1;
+    max-width: initial;
+  }
+}
+
+.filtered-search-token,
+.filtered-search-term {
+  display: -webkit-flex;
+  display: flex;
+  margin-top: 5px;
+  margin-bottom: 5px;
+
+  .selectable {
+    display: -webkit-flex;
+    display: flex;
+  }
+
+  .name,
+  .value {
+    display: inline-block;
+    padding: 2px 7px;
+  }
+
+  .name {
+    background-color: $filter-name-resting-color;
+    color: $filter-name-text-color;
+    border-radius: 2px 0 0 2px;
+    margin-right: 1px;
+    text-transform: capitalize;
+  }
+
+  .value {
+    background-color: $white-normal;
+    color: $filter-value-text-color;
+    border-radius: 0 2px 2px 0;
+    margin-right: 5px;
+  }
+
+  .selected {
+    .name {
+      background-color: $filter-name-selected-color;
+    }
+
+    .value {
+      background-color: $filter-value-selected-color;
+    }
+  }
+}
+
+.filtered-search-term {
+  .name {
+    background-color: inherit;
+    color: $black;
+    text-transform: none;
+  }
+
+  .selectable {
+    cursor: text;
+  }
+}
+
+.scroll-container {
+  display: -webkit-flex;
+  display: flex;
+  overflow-x: auto;
+  white-space: nowrap;
+  width: 100%;
 }
 
 .filtered-search-input-container {
@@ -33,13 +156,48 @@
   display: flex;
   position: relative;
   width: 100%;
+  border: 1px solid $border-color;
+  background-color: $white-light;
+
+  @media (max-width: $screen-xs-min) {
+    -webkit-flex: 1 1 auto;
+    flex: 1 1 auto;
+    margin-bottom: 10px;
+
+    .dropdown-menu {
+      width: auto;
+      left: 0;
+      right: 0;
+      max-width: none;
+      min-width: 100%;
+    }
+  }
+
+  &:hover {
+    @extend .form-control:hover;
+  }
+
+  &.focus,
+  &.focus:hover {
+    border-color: $dropdown-input-focus-border;
+    box-shadow: 0 0 4px $search-input-focus-shadow-color;
+  }
+
+  &.focus .fa-filter {
+    color: $common-gray-dark;
+  }
 
   .form-control {
-    padding-left: 25px;
-    padding-right: 25px;
+    position: relative;
+    min-width: 200px;
+    padding: 5px 25px 6px 0;
+    border-color: transparent;
 
-    &:focus ~ .fa-filter {
-      color: $common-gray-dark;
+    &:focus,
+    &:hover {
+      outline: none;
+      border-color: transparent;
+      box-shadow: none;
     }
   }
 
@@ -57,12 +215,13 @@
 
   .clear-search {
     width: 35px;
-    background-color: transparent;
+    background-color: $white-light;
     border: none;
     position: absolute;
     right: 0;
     height: 100%;
     outline: none;
+    z-index: 1;
 
     &:hover .fa-times {
       color: $common-gray-dark;
@@ -70,6 +229,15 @@
   }
 }
 
+.filter-dropdown-container {
+  display: -webkit-flex;
+  display: flex;
+
+  .dropdown-toggle {
+    line-height: 22px;
+  }
+}
+
 .dropdown-menu .filter-dropdown-item {
   padding: 0;
 }
@@ -79,6 +247,41 @@
   overflow: auto;
 }
 
+@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+  .issues-details-filters {
+    .dropdown-menu-toggle {
+      width: 100px;
+    }
+  }
+}
+
+@media (max-width: $screen-xs-max) {
+  .issues-details-filters {
+    padding: 0 0 10px;
+    background-color: $white-light;
+    border-top: 0;
+  }
+}
+
+@media (max-width: $screen-xs) {
+  .filter-dropdown-container {
+    .dropdown-toggle,
+    .dropdown {
+      width: 100%;
+    }
+
+    .dropdown {
+      margin-left: 0;
+    }
+
+    .fa-chevron-down {
+      position: absolute;
+      right: 10px;
+      top: 10px;
+    }
+  }
+}
+
 %filter-dropdown-item-btn-hover {
   background-color: $dropdown-hover-color;
   color: $white-light;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 78434b99b62120eb25bac6cb2f61a7641825e2d4..6660a0222607bbce625ccc8ed27ad58287775aee 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -149,14 +149,14 @@ header {
 
     .header-logo {
       display: inline-block;
-      margin: 0 8px 0 3px;
+      margin: 0 7px 0 2px;
       position: relative;
-      top: 7px;
+      top: 10px;
       transition-duration: .3s;
 
       svg,
       img {
-        height: 36px;
+        height: 28px;
       }
 
       &:hover {
@@ -164,12 +164,25 @@ header {
       }
     }
 
+    .group-name-toggle {
+      margin: 0 5px;
+      vertical-align: sub;
+    }
+
+    .group-title {
+      &.is-hidden {
+        .hidable:not(:last-of-type) {
+          display: none;
+        }
+      }
+    }
+
     .title {
       position: relative;
       padding-right: 20px;
       margin: 0;
       font-size: 18px;
-      max-width: 450px;
+      max-width: 385px;
       display: inline-block;
       line-height: $header-height;
       font-weight: normal;
@@ -179,10 +192,26 @@ header {
       vertical-align: top;
       white-space: nowrap;
 
+      &.initializing {
+        display: none;
+      }
+
+      @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+        max-width: 300px;
+      }
+
       @media (max-width: $screen-xs-max) {
         max-width: 190px;
       }
 
+      @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+        max-width: 428px;
+      }
+
+      @media (min-width: $screen-lg-min) {
+        max-width: 685px;
+      }
+
       a {
         color: $gl-text-color;
 
@@ -253,24 +282,34 @@ header {
     font-size: 18px;
 
     .navbar-nav {
+      display: table;
+      table-layout: fixed;
+      width: 100%;
       margin: 0;
-      float: none !important;
-
-      .visible-xs,
-      .visible-sm {
-        display: table-cell !important;
-      }
+      text-align: right;
     }
 
     .navbar-collapse {
       padding-left: 5px;
 
-      .nav > li {
-        display: table-cell;
-        width: 1%;
+      .nav > li:not(.hidden-xs) {
+        display: table-cell!important;
+        width: 25%;
+
+        a {
+          margin-right: 8px;
+        }
       }
     }
   }
+
+  .header-user-dropdown-toggle {
+    text-align: center;
+  }
+
+  .header-user-avatar {
+    float: none;
+  }
 }
 
 .header-user {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 909a0f4afda0133773da62ba10c5eb62e57024c8..6d27d7568cf29397f5a0b1a735882440ae3b7cca 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -57,8 +57,13 @@
         visibility: hidden;
       }
 
-      &:hover i {
-        visibility: visible;
+      &:hover,
+      &:focus {
+        outline: none;
+
+        & i {
+          visibility: visible;
+        }
       }
     }
   }
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index d335fedefe2a35d698a3f8c41f420be3e94df8eb..300ba4f2de62cd5d5a0a390687a61e98757f6571 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,17 +2,6 @@
   font-family: $regular_font;
   font-size: $font-size-base;
 
-  &.ui-autocomplete {
-    border-color: $jq-ui-border;
-    padding: 0;
-    margin-top: 2px;
-    z-index: 1001;
-
-    .ui-menu-item a {
-      padding: 4px 10px;
-    }
-  }
-
   .ui-state-default {
     border: 1px solid $white-light;
     background: $white-light;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 29d55c446992e3b5a11ac50aecc4f9784b64651d..0a42b17c1f5519b829833d132bb33715d5320c08 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -8,6 +8,19 @@ body {
   &.navless {
     background-color: $white-light !important;
   }
+
+  &.card-content {
+    background-color: $gray-darker;
+
+    .content-wrapper {
+      padding: 0;
+
+      .container-fluid,
+      .container-limited {
+        background-color: $gray-darker;
+      }
+    }
+  }
 }
 
 .container {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2bfdb9f96017f74d660c569b6f2e71b7fe174948..7adbb0a4188d6874e4f6a8801e296604d2165f19 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -96,16 +96,6 @@ ul.unstyled-list > li {
   border-bottom: none;
 }
 
-ul.task-list {
-  li.task-list-item {
-    list-style-type: none;
-  }
-
-  ul:not(.task-list) {
-    padding-left: 1.3em;
-  }
-}
-
 // Generic content list
 ul.content-list {
   @include basic-list;
@@ -239,44 +229,6 @@ ul.content-list {
   }
 }
 
-// Table list
-.table-list {
-  display: table;
-  width: 100%;
-
-  .table-list-row {
-    display: table-row;
-  }
-
-  .table-list-cell {
-    display: table-cell;
-    vertical-align: top;
-    padding: 10px 16px;
-    border-bottom: 1px solid $gray-darker;
-
-    &.avatar-cell {
-      width: 36px;
-      padding-right: 0;
-
-      img {
-        margin-right: 0;
-      }
-    }
-  }
-
-  &.table-wide {
-    .table-list-cell {
-      &:last-of-type {
-        padding-right: 0;
-      }
-
-      &:first-of-type {
-        padding-left: 0;
-      }
-    }
-  }
-}
-
 .panel > .content-list > li {
   padding: $gl-padding-top $gl-padding;
 }
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index d4758d9035208b6e4d5e51aae57f671fab4be8b5..a668a6c4c398e8075a2438865640b76529d20b0e 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -147,6 +147,9 @@
 }
 
 .atwho-view {
+  overflow-y: auto;
+  overflow-x: hidden;
+
   small.description {
     float: right;
     padding: 3px 5px;
@@ -162,4 +165,8 @@
       @include disableAllAnimation;
     }
   }
+
+  ul > li {
+    white-space: nowrap;
+  }
 }
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1acd06122a3dde51ce2e9aceb1ef58b9b8c58975..df78bbdea510558066d9d9b5991bdfab1b05a53d 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -76,6 +76,13 @@
   #{$property}: $value;
 }
 
+/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */
+@mixin on-webkit-only {
+  @media screen and (-webkit-min-device-pixel-ratio:0) {
+    @content;
+  }
+}
+
 @mixin keyframes($animation-name) {
   @-webkit-keyframes #{$animation-name} {
     @content;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8e2c56a84885f940bc868c7b65846336e63e89f6..eb73f7cc794d7039805a2822a74ef58a130d202a 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -100,8 +100,7 @@
 
 @media (max-width: $screen-sm-max) {
   .issues-filters {
-    .milestone-filter,
-    .labels-filter {
+    .milestone-filter {
       display: none;
     }
   }
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7d4a814a36c0f7bf07ecf1e98bfedee501ed7372..205d23b13291dd6e9561d389edbe762dcdc0f1a6 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,7 +1,7 @@
 @mixin fade($gradient-direction, $gradient-color) {
   visibility: hidden;
   opacity: 0;
-  z-index: 1;
+  z-index: 2;
   position: absolute;
   bottom: 12px;
   width: 43px;
@@ -18,7 +18,7 @@
 
   .fa {
     position: relative;
-    top: 6px;
+    top: 5px;
     font-size: 18px;
   }
 }
@@ -79,6 +79,7 @@
   }
 
   &.sub-nav {
+    text-align: center;
     background-color: $gray-normal;
 
     .container-fluid {
@@ -137,7 +138,6 @@
 
   .nav-links {
     display: inline-block;
-    width: 50%;
     margin-bottom: 0;
     border-bottom: none;
 
@@ -286,13 +286,14 @@
   background: $gray-light;
   border-bottom: 1px solid $border-color;
   transition: padding $sidebar-transition-duration;
+  text-align: center;
 
   .container-fluid {
     position: relative;
 
     .nav-control {
       @media (max-width: $screen-sm-max) {
-        margin-right: 75px;
+        margin-right: 2px;
       }
     }
   }
@@ -351,7 +352,7 @@
     right: -5px;
 
     .fa {
-      right: -28px;
+      right: -7px;
     }
   }
 
@@ -381,7 +382,7 @@
       left: 0;
 
       .fa {
-        left: -4px;
+        left: 10px;
       }
     }
   }
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index efe93724013edba08e6af39101574251c2291e75..9d8d08dff8878ea0376848e39f10d16385dd94c7 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -48,11 +48,3 @@
     line-height: inherit;
   }
 }
-
-.panel-default {
-  .table-list-row:last-child {
-    .table-list-cell {
-      border-bottom: 0;
-    }
-  }
-}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d09b1c9d7f5e9fab87756ad84efd11fe19ee2e48..40e93032f59c485377f77ece4d8152de28a1818f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -55,7 +55,7 @@
   padding-right: 0;
 
   @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
-    &:not(.build-sidebar):not(.wiki-sidebar) {
+    .content-wrapper {
       padding-right: $gutter_collapsed_width;
     }
   }
@@ -73,10 +73,6 @@
       right: $gutter_collapsed_width;
     }
   }
-
-  &.with-overlay {
-    padding-right: $gutter_collapsed_width;
-  }
 }
 
 .right-sidebar {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index ea2d26dd5a00b968fde2755651b415bd59a67d5c..12a86a6464523c3c686778aba6244ff0aa4ddcfb 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -86,6 +86,16 @@
   position: fixed;
 }
 
+/*
+ * Fix <summary> elements on firefox
+ * See https://github.com/necolas/normalize.css/issues/640
+ * and https://github.com/twbs/bootstrap/issues/21060
+ *
+ */
+summary {
+  display: list-item;
+}
+
 @import "bootstrap/responsive-utilities";
 
 // Labels
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 54958973f154a4ddbcead6ad3c4cfe64caf280e4..c241816788b4f9ee361fde5c5a5f9a4601dfbcbc 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -134,7 +134,7 @@
   ul,
   ol {
     padding: 0;
-    margin: 3px 0 3px 28px !important;
+    margin: 3px 0 !important;
   }
 
   ul:dir(rtl),
@@ -144,6 +144,29 @@
 
   li {
     line-height: 1.6em;
+    margin-left: 25px;
+    padding-left: 3px;
+
+    /* Normalize the bullet position on webkit. */
+    @include on-webkit-only {
+      margin-left: 28px;
+      padding-left: 0;
+    }
+  }
+
+  ul.task-list {
+    li.task-list-item {
+      list-style-type: none;
+      position: relative;
+      padding-left: 28px;
+      margin-left: 0 !important;
+
+      input.task-list-item-checkbox {
+        position: absolute;
+        left: 8px;
+        top: 5px;
+      }
+    }
   }
 
   a[href*="/uploads/"],
@@ -283,6 +306,11 @@ a > code {
  * Textareas intended for GFM
  *
  */
+textarea.js-gfm-input {
+  font-family: $monospace_font;
+  font-size: 13px;
+}
+
 .strikethrough {
   text-decoration: line-through;
 }
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ba0af072716f14961457208e338c4d5d563248cf..6841adb637e9e6730a5ef247908b14299f6d6ad8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1;
  * Fonts
  */
 $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 
 /*
 * Dropdowns
@@ -540,3 +540,12 @@ Pipeline Graph
 $stage-hover-bg: #eaf3fc;
 $stage-hover-border: #d1e7fc;
 $action-icon-color: #d6d6d6;
+
+/*
+Filtered Search
+*/
+$filter-name-resting-color: #f8f8f8;
+$filter-name-text-color: rgba(0, 0, 0, 0.55);
+$filter-value-text-color: rgba(0, 0, 0, 0.85);
+$filter-name-selected-color: #ebebeb;
+$filter-value-selected-color: #d7d7d7;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 97ade638db6be1a8d9c9a6caf6ad0e0db8e905ec..0c226ff75980f47df454038cc93cf0fa3193d422 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -20,8 +20,9 @@
       outline: none;
       resize: none;
       height: 100vh;
+      max-height: calc(100vh - 10px);
       max-width: 900px;
-      margin: 0 auto;
+      margin: 0 auto 10px;
     }
 
     .zen-control-leave {
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6f2e746d4b01bb77084418de41f1a70072cced5f..09951fe3d3e38bb649087a2f1c67790142606cf0 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -20,6 +20,8 @@ $dark-highlight-bg: #ffe792;
 $dark-highlight-color: $black;
 $dark-pre-hll-bg: #373b41;
 $dark-hll-bg: #373b41;
+$dark-over-bg: #9f9ab5;
+$dark-expanded-bg: #3e3e3e;
 $dark-c: #969896;
 $dark-err: #c66;
 $dark-k: #b294bb;
@@ -139,9 +141,37 @@ $dark-il: #de935f;
       }
     }
 
+    .diff-line-num {
+      &.is-over,
+      &.hll:not(.empty-cell).is-over {
+        background-color: $dark-over-bg;
+        border-color: darken($dark-over-bg, 5%);
+
+        a {
+          color: darken($dark-over-bg, 15%);
+        }
+      }
+    }
+
     .line_content.match {
       @include dark-diff-match-line;
     }
+
+    &:not(.diff-expanded) + .diff-expanded,
+    &.diff-expanded + .line_holder:not(.diff-expanded) {
+      > .diff-line-num,
+      > .line_content {
+        border-top: 1px solid $black;
+      }
+    }
+
+    &.diff-expanded {
+      > .diff-line-num,
+      > .line_content {
+        background: $dark-expanded-bg;
+        border-color: $dark-expanded-bg;
+      }
+    }
   }
 
   // highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 2144a5f746605bfa70a218776e0f35fa0ce29b05..b6a6d298adf592b2af502ceb063c9b2e699f5fd3 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -13,6 +13,8 @@ $monokai-line-empty-bg: #49483e;
 $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
 $monokai-diff-border: #808080;
 $monokai-highlight-bg: #ffe792;
+$monokai-over-bg: #9f9ab5;
+$monokai-expanded-bg: #3e3e3e;
 
 $monokai-new-bg: rgba(166, 226, 46, 0.1);
 $monokai-new-idiff: rgba(166, 226, 46, 0.15);
@@ -139,9 +141,37 @@ $monokai-gi: #a6e22e;
       }
     }
 
+    .diff-line-num {
+      &.is-over,
+      &.hll:not(.empty-cell).is-over {
+        background-color: $monokai-over-bg;
+        border-color: darken($monokai-over-bg, 5%);
+
+        a {
+          color: darken($monokai-over-bg, 15%);
+        }
+      }
+    }
+
     .line_content.match {
       @include dark-diff-match-line;
     }
+
+    &:not(.diff-expanded) + .diff-expanded,
+    &.diff-expanded + .line_holder:not(.diff-expanded) {
+      > .diff-line-num,
+      > .line_content {
+        border-top: 1px solid $black;
+      }
+    }
+
+    &.diff-expanded {
+      > .diff-line-num,
+      > .line_content {
+        background: $monokai-expanded-bg;
+        border-color: $monokai-expanded-bg;
+      }
+    }
   }
 
   // highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 2cb1d18f12f55f384783ced977f8e333273ce0be..4f7a50dcb4fdf3888c772c8594ec2f69e4d1b922 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -17,6 +17,8 @@ $solarized-dark-line-color-new: #5a766c;
 $solarized-dark-line-color-old: #7a6c71;
 $solarized-dark-highlight: #094554;
 $solarized-dark-hll-bg: #174652;
+$solarized-dark-over-bg: #9f9ab5;
+$solarized-dark-expanded-bg: #010d10;
 $solarized-dark-c: #586e75;
 $solarized-dark-err: #93a1a1;
 $solarized-dark-g: #93a1a1;
@@ -143,9 +145,37 @@ $solarized-dark-il: #2aa198;
       }
     }
 
+    .diff-line-num {
+      &.is-over,
+      &.hll:not(.empty-cell).is-over {
+        background-color: $solarized-dark-over-bg;
+        border-color: darken($solarized-dark-over-bg, 5%);
+
+        a {
+          color: darken($solarized-dark-over-bg, 15%);
+        }
+      }
+    }
+
     .line_content.match {
       @include dark-diff-match-line;
     }
+
+    &:not(.diff-expanded) + .diff-expanded,
+    &.diff-expanded + .line_holder:not(.diff-expanded) {
+      > .diff-line-num,
+      > .line_content {
+        border-top: 1px solid $black;
+      }
+    }
+
+    &.diff-expanded {
+      > .diff-line-num,
+      > .line_content {
+        background: $solarized-dark-expanded-bg;
+        border-color: $solarized-dark-expanded-bg;
+      }
+    }
   }
 
   // highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index b72c432673003cdb0fc4d04fdbbc57db1b051184..6463fe96c1b8ddb0b8a99aacb17b12aaec1b4c6f 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -18,6 +18,9 @@ $solarized-light-line-color-new: #a1a080;
 $solarized-light-line-color-old: #ad9186;
 $solarized-light-highlight: #eee8d5;
 $solarized-light-hll-bg: #ddd8c5;
+$solarized-light-over-bg: #ded7fc;
+$solarized-light-expanded-border: #d2cdbd;
+$solarized-light-expanded-bg: #ece6d4;
 $solarized-light-c: #93a1a1;
 $solarized-light-err: #586e75;
 $solarized-light-g: #586e75;
@@ -150,9 +153,37 @@ $solarized-light-il: #2aa198;
       }
     }
 
+    .diff-line-num {
+      &.is-over,
+      &.hll:not(.empty-cell).is-over {
+        background-color: $solarized-light-over-bg;
+        border-color: darken($solarized-light-over-bg, 5%);
+
+        a {
+          color: darken($solarized-light-over-bg, 15%);
+        }
+      }
+    }
+
     .line_content.match {
       @include matchLine;
     }
+
+    &:not(.diff-expanded) + .diff-expanded,
+    &.diff-expanded + .line_holder:not(.diff-expanded) {
+      > .diff-line-num,
+      > .line_content {
+        border-top: 1px solid $solarized-light-expanded-border;
+      }
+    }
+
+    &.diff-expanded {
+      > .diff-line-num,
+      > .line_content {
+        background: $solarized-light-expanded-bg;
+        border-color: $solarized-light-expanded-bg;
+      }
+    }
   }
 
   // highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 398fbfd3b18bad37ee78bd53329180705002e8ca..ab2018bfbca795df7b7182b51e453bdc3ef3cb15 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -7,6 +7,9 @@ $white-code-color: $gl-text-color;
 $white-highlight: #fafe3d;
 $white-pre-hll-bg: #f8eec7;
 $white-hll-bg: #f8f8f8;
+$white-over-bg: #ded7fc;
+$white-expanded-border: #e0e0e0;
+$white-expanded-bg: #f7f7f7;
 $white-c: #998;
 $white-err: #a61717;
 $white-err-bg: #e3d2d2;
@@ -123,12 +126,38 @@ $white-gc-bg: #eaf2f5;
         }
       }
 
+      &.is-over,
+      &.hll:not(.empty-cell).is-over {
+        background-color: $white-over-bg;
+        border-color: darken($white-over-bg, 5%);
+
+        a {
+          color: darken($white-over-bg, 15%);
+        }
+      }
+
       &.hll:not(.empty-cell) {
         background-color: $line-number-select;
         border-color: $line-select-yellow-dark;
       }
     }
 
+    &:not(.diff-expanded) + .diff-expanded,
+    &.diff-expanded + .line_holder:not(.diff-expanded) {
+      > .diff-line-num,
+      > .line_content {
+        border-top: 1px solid $white-expanded-border;
+      }
+    }
+
+    &.diff-expanded {
+      > .diff-line-num,
+      > .line_content {
+        background: $white-expanded-bg;
+        border-color: $white-expanded-bg;
+      }
+    }
+
     .line_content {
       &.old {
         background-color: $line-removed;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9a36d76136b20330b2f9a466e8e0f402378195b2..f9ee33019cd7b24d2ee57655a5d64469106f10fc 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -420,12 +420,9 @@
   display: -webkit-flex;
   display: flex;
 
-  .form-control {
-    margin-left: auto;
-
-    @media (min-width: $screen-sm-min) {
-      max-width: 200px;
-    }
+  .issues-filters {
+    -webkit-flex: 1;
+    flex: 1;
   }
 }
 
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c3d45d708c1b88239879a620242d1ee58107d560..da8410eca66c4f1d22f68a88b6b4624f99aa7ca7 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -38,6 +38,38 @@
   }
 }
 
+.pipeline-info {
+  .status-icon-container {
+    display: inline-block;
+    vertical-align: middle;
+    margin-right: 3px;
+
+    svg {
+      display: block;
+      width: 22px;
+      height: 22px;
+    }
+  }
+
+  .mr-widget-pipeline-graph {
+    display: inline-block;
+    vertical-align: middle;
+    margin: 0 -6px 0 0;
+
+    .dropdown-menu {
+      margin-top: 11px;
+    }
+  }
+}
+
+.branch-info .commit-icon {
+  margin-right: 3px;
+
+  svg {
+    top: 3px;
+  }
+}
+
 /*
  * Commit message textarea for web editor and
  * custom merge request message
@@ -78,6 +110,7 @@
   padding: 5px 10px;
   background-color: $gray-light;
   border-bottom: 1px solid $gray-darker;
+  border-top: 1px solid $gray-darker;
   font-size: 14px;
 
   &:first-child {
@@ -117,10 +150,37 @@
   }
 }
 
+.commit.flex-list {
+  display: flex;
+}
+
+.avatar-cell {
+  width: 46px;
+  padding-left: 10px;
+
+  img {
+    margin-right: 0;
+  }
+}
+
+.commit-detail {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  flex-grow: 1;
+  padding-left: 10px;
+
+  .merge-request-branches & {
+    flex-direction: column;
+  }
+}
+
+.commit-content {
+  padding-right: 10px;
+}
+
 .commit-actions {
   @media (min-width: $screen-sm-min) {
-    width: 300px;
-    text-align: right;
     font-size: 0;
   }
 
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 5b777953fb0171c36da5374e30388a3ca022c88d..ad3dbc7ac4832f60102d1cecbd7c0f578307b7f6 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -369,13 +369,11 @@
     // Custom CSS for components
     .item-conmmit-component {
       .commit-icon {
-        position: relative;
-        top: 3px;
-        left: 1px;
-        display: inline-block;
-
         svg {
-          float: left;
+          display: inline-block;
+          width: 20px;
+          height: 20px;
+          vertical-align: bottom;
         }
       }
     }
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 92d7772da5733a28e2a8eaccb5f7f658176868ad..eab79c2a48187627af1f26c17feeac74ed82a9ac 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -89,6 +89,10 @@
 
       .diff-line-num {
         width: 50px;
+
+        a {
+          transition: none;
+        }
       }
 
       .line_holder td {
@@ -111,7 +115,7 @@
       }
 
       .add-diff-note {
-        margin-left: -65px;
+        margin-left: -55px;
       }
     }
 
@@ -133,8 +137,13 @@
         width: 35px;
         font-weight: normal;
 
-        &:hover {
-          text-decoration: underline;
+        &[disabled] {
+          cursor: default;
+
+          &:hover,
+          &:active {
+            text-decoration: none;
+          }
         }
       }
     }
@@ -485,3 +494,103 @@
     }
   }
 }
+
+.diff-comment-avatar-holders {
+  position: absolute;
+  height: 19px;
+  width: 19px;
+  margin-left: -15px;
+
+  &:hover {
+    .diff-comment-avatar,
+    .diff-comments-more-count {
+      @for $i from 1 through 4 {
+        $x-pos: 14px;
+
+        &:nth-child(#{$i}) {
+          @if $i == 4 {
+            $x-pos: 14.5px;
+          }
+
+          transform: translateX((($i * $x-pos) - $x-pos));
+
+          &:hover {
+            transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
+          }
+        }
+      }
+    }
+
+    .diff-comments-more-count {
+      padding-left: 2px;
+      padding-right: 2px;
+      width: auto;
+    }
+  }
+}
+
+.diff-comment-avatar,
+.diff-comments-more-count {
+  position: absolute;
+  left: 0;
+  width: 19px;
+  height: 19px;
+  margin-right: 0;
+  border-color: $white-light;
+  cursor: pointer;
+  transition: all .1s ease-out;
+
+  @for $i from 1 through 4 {
+    &:nth-child(#{$i}) {
+      z-index: (4 - $i);
+    }
+  }
+}
+
+.diff-comments-more-count {
+  width: 19px;
+  min-width: 19px;
+  padding-left: 0;
+  padding-right: 0;
+  overflow: hidden;
+}
+
+.diff-comments-more-count,
+.diff-notes-collapse {
+  background-color: $gray-darkest;
+  color: $white-light;
+  border: 1px solid $white-light;
+  border-radius: 1em;
+  font-family: $regular_font;
+  font-size: 9px;
+  line-height: 17px;
+  text-align: center;
+}
+
+.diff-notes-collapse {
+  position: relative;
+  width: 19px;
+  height: 19px;
+  padding: 0;
+  transition: transform .1s ease-out;
+
+  svg {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    margin-left: -5.5px;
+    margin-top: -5.5px;
+  }
+
+  path {
+    fill: $white-light;
+  }
+
+  &:hover {
+    transform: scale(1.2);
+  }
+
+  &:focus {
+    outline: 0;
+  }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 181dcb7721f5002e0ebfd32bde68bb678cd360f5..73a5da715f21e62c7e1df8889983f69e2fefc57e 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -15,96 +15,97 @@
   padding-top: 20px;
 }
 
-@media (max-width: $screen-xs-max) {
-  .environments-container {
+.environments-container {
+  .table-holder {
     width: 100%;
     overflow: auto;
   }
-}
-
-.environments {
-  table-layout: fixed;
 
-  .environments-commit,
-  .environments-actions,
-  .environments-deploy,
-  .environments-build,
-  .environments-date {
-    position: static;
-    float: none;
-    display: table-cell;
-  }
+  .table.ci-table {
+    .environments-actions {
+      min-width: 200px;
+    }
 
-  .environments-name,
-  .environments-commit,
-  .environments-actions {
-    width: 20%;
-  }
+    .environments-commit,
+    .environments-actions {
+      width: 20%;
+    }
 
-  .environments-date {
-    width: 10%;
-  }
+    .environments-date {
+      width: 10%;
+    }
 
-  .environments-deploy,
-  .environments-build {
-    width: 15%;
-  }
+    .environments-name,
+    .environments-deploy,
+    .environments-build {
+      width: 15%;
+    }
 
-  .environment-name,
-  .environments-build-cell,
-  .deployment-column {
-    word-break: break-all;
-  }
+    .deployment-column {
+      > span {
+        word-break: break-all;
+      }
 
-  .deployment-column {
-    .avatar {
-      float: none;
+      .avatar {
+        float: none;
+      }
     }
-  }
 
-  .commit-title {
-    margin: 0;
-  }
+    .btn-group {
 
-  .avatar-image-container {
-    text-decoration: none;
-  }
+      > a {
+        color: $gl-text-color-secondary;
+      }
 
-  .icon-play {
-    height: 13px;
-    width: 12px;
-  }
+      svg path {
+        fill: $gl-text-color-secondary;
+      }
 
-  .external-url,
-  .dropdown-new {
-    color: $gl-text-color-secondary;
-  }
+      .dropdown {
+        outline: none;
+      }
+    }
 
-  .dropdown-menu {
+    .commit-title {
+      margin: 0;
+    }
 
-    .fa {
-      margin-right: 6px;
-      color: $gl-text-color-secondary;
+    .avatar-image-container {
+      text-decoration: none;
     }
-  }
 
-  .build-link,
-  .branch-name {
-    color: $gl-text-color;
-  }
+    .icon-play {
+      height: 13px;
+      width: 12px;
+    }
 
-  .stop-env-link,
-  .external-url {
-    color: $gl-text-color-secondary;
+    .external-url,
+    .dropdown-new {
+      color: $gl-text-color-secondary;
+    }
 
-    .stop-env-icon {
-      font-size: 14px;
+    .dropdown-menu {
+      .fa {
+        margin-right: 6px;
+        color: $gl-text-color-secondary;
+      }
     }
-  }
 
-  .deployment {
-    .build-column {
+    .build-link,
+    .branch-name {
+      color: $gl-text-color;
+    }
+
+    .stop-env-link,
+    .external-url {
+      color: $gl-text-color-secondary;
+
+      .stop-env-icon {
+        font-size: 14px;
+      }
+    }
 
+    .deployment .build-column {
       .build-link {
         color: $gl-text-color;
       }
@@ -113,34 +114,108 @@
         float: none;
       }
     }
-  }
-
-  .folder-icon {
-    margin-right: 3px;
-    color: $gl-text-color-secondary;
-    display: inline-block;
 
-    .fa:nth-child(1) {
+    .folder-icon {
       margin-right: 3px;
+      color: $gl-text-color-secondary;
+      display: inline-block;
+
+      .fa:nth-child(1) {
+        margin-right: 3px;
+      }
+    }
+
+    .folder-name {
+      cursor: pointer;
+      color: $gl-text-color-secondary;
+      display: inline-block;
     }
-  }
 
-  .folder-name {
-    cursor: pointer;
-    color: $gl-text-color-secondary;
-    display: inline-block;
+    .icon-container {
+      width: 20px;
+      text-align: center;
+    }
+
+    .branch-commit {
+      .commit-id {
+        margin-right: 0;
+      }
+    }
+
+    .no-btn {
+      border: none;
+      background: none;
+      outline: none;
+      width: 100%;
+      text-align: left;
+    }
   }
 }
 
-.table.ci-table.environments {
-  .icon-container {
-    width: 20px;
-    text-align: center;
+.prometheus-graph {
+  text {
+    fill: $stat-graph-axis-fill;
   }
+}
 
-  .branch-commit {
-    .commit-id {
-      margin-right: 0;
-    }
+.x-axis path,
+.y-axis path,
+.label-x-axis-line,
+.label-y-axis-line {
+  fill: none;
+  stroke-width: 1;
+  shape-rendering: crispEdges;
+}
+
+.x-axis path,
+.y-axis path {
+  stroke: $stat-graph-axis-fill;
+}
+
+.label-x-axis-line,
+.label-y-axis-line {
+  stroke: $border-color;
+}
+
+.y-axis {
+  line {
+    stroke: $stat-graph-axis-fill;
+    stroke-width: 1;
   }
 }
+
+.metric-area {
+  opacity: 0.8;
+}
+
+.prometheus-graph-overlay {
+  fill: none;
+  opacity: 0.0;
+  pointer-events: all;
+}
+
+.rect-text-metric {
+  fill: $white-light;
+  stroke-width: 1;
+  stroke: $black;
+}
+
+.rect-axis-text {
+  fill: $white-light;
+}
+
+.text-metric,
+.text-median-metric,
+.text-metric-usage,
+.text-metric-date {
+  fill: $black;
+}
+
+.text-metric-date {
+  font-weight: 200;
+}
+
+.selected-metric-line {
+  stroke: $black;
+  stroke-width: 1;
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 5776d86983ae0ed287c4d1171464e8f1912a4d23..08398bb43a27ddbd1ff2f9e1c6cc38634a1a7120 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -155,7 +155,7 @@
 
 @media (max-width: $screen-xs-max) {
   .event-item {
-    padding-left: $gl-padding;
+    padding-left: 0;
 
     .event-title {
       white-space: normal;
@@ -169,8 +169,7 @@
 
     .event-body {
       margin: 0;
-      border-left: 2px solid $events-body-border;
-      padding-left: 10px;
+      padding-left: 0;
     }
 
     .event-item-timestamp {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index d377526e655e820a47259b76a6ed6bb1fe2d29d3..84d21e48463740dd3e9dad485a6397af4caf3cd4 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -73,3 +73,19 @@
     }
   }
 }
+
+.mattermost-icon svg {
+  width: 16px;
+  height: 16px;
+  vertical-align: text-bottom;
+}
+
+.mattermost-team-name {
+  color: $gl-text-color-secondary;
+}
+
+.mattermost-info {
+  display: block;
+  color: $gl-text-color-secondary;
+  margin-top: 10px;
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 80b0c9493d8906610309a75db7795919a0f977ff..cb7ebd61504bef63b8bc51f9f293aea950273b99 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -10,6 +10,11 @@
     .issue-labels {
       display: inline-block;
     }
+
+    .icon-merge-request-unmerged {
+      height: 13px;
+      margin-bottom: 3px;
+     }
   }
 }
 
@@ -51,7 +56,10 @@ ul.related-merge-requests > li {
 
 .merge-request-id {
   display: inline-block;
-  width: 3em;
+}
+
+.merge-request-info {
+  margin-left: 5px;
 }
 
 .merge-request-status {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0b0c4bc130d5a99858a243f8d2a2423f767caf22..7c3172421c1b351f50b959d13381d5b01ea42299 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,7 +3,6 @@
  *
  */
 .mr-state-widget {
-  background: $gray-light;
   color: $gl-text-color;
   border: 1px solid $border-color;
   border-radius: 2px;
@@ -25,11 +24,7 @@
         color: inherit;
       }
 
-      .btn-success.dropdown-toggle:disabled {
-        background-color: $gl-success;
-      }
-
-      .accept_merge_request {
+      .accept-merge-request {
         &.ci-pending,
         &.ci-running {
           @include btn-blue;
@@ -42,6 +37,12 @@
           @include btn-red;
         }
       }
+
+      .dropdown-toggle {
+        .fa {
+          color: inherit;
+        }
+      }
     }
 
     .accept-control {
@@ -103,12 +104,17 @@
     @media (max-width: $screen-xs-max) {
       flex-wrap: wrap;
     }
+
+    .ci-status-icon > .icon-link > svg {
+      width: 22px;
+      height: 22px;
+    }
   }
 
   .mr-widget-body,
   .ci_widget,
   .mr-widget-footer {
-    padding: $gl-padding;
+    padding: 16px;
   }
 
   .mr-widget-pipeline-graph {
@@ -168,10 +174,6 @@
       }
     }
 
-    p:last-child {
-      margin-bottom: 0;
-    }
-
     .btn-grouped {
       margin-left: 0;
       margin-right: 7px;
@@ -234,8 +236,7 @@
 
   .commit {
     margin: 0;
-    padding-top: 2px;
-    padding-bottom: 2px;
+    padding: 10px 0;
     list-style: none;
 
     &:hover {
@@ -334,8 +335,61 @@
   }
 }
 
+.remove-message-pipes {
+  ul {
+    margin: 10px 0 0 12px;
+    padding: 0;
+    list-style: none;
+    border-left: 2px solid $border-color;
+    display: inline-block;
+  }
+
+  li {
+    position: relative;
+    margin: 0;
+    padding: 0;
+    display: block;
+
+    span {
+      margin-left: 15px;
+      max-height: 20px;
+    }
+  }
+
+  li::before {
+    content: '';
+    position: absolute;
+    border-top: 2px solid $border-color;
+    height: 1px;
+    top: 8px;
+    width: 8px;
+  }
+
+  li:last-child {
+    &::before {
+      top: 18px;
+    }
+
+    span {
+      display: block;
+      position: relative;
+      top: 5px;
+      margin-top: 5px;
+    }
+  }
+}
+
 .mr-source-target {
+  background-color: $gray-light;
   line-height: 31px;
+  border-style: solid;
+  border-width: 1px;
+  border-color: $border-color;
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+  border-bottom: none;
+  padding: 16px;
+  margin-bottom: -1px;
 }
 
 .panel-new-merge-request {
@@ -350,7 +404,7 @@
   }
 
   .panel-footer {
-    padding: 5px 10px;
+    padding: 0;
 
     .btn {
       min-width: auto;
@@ -420,6 +474,11 @@
   }
 }
 
+.assign-to-me-link {
+  padding-left: 12px;
+  white-space: nowrap;
+}
+
 .table-holder {
   .ci-table {
 
@@ -431,6 +490,8 @@
 }
 
 .merged-buttons {
+  margin-top: 20px;
+
   .btn {
     float: left;
 
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f984b469609731a480e3cfc5aea03d4edb557bda..927bf9805cec1456ba87f2cfe470f938cb2ea223 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
 .error-alert > .alert {
   margin-top: 5px;
   margin-bottom: 5px;
+
+  &.alert-dismissable {
+    .close {
+      color: $white-light;
+      opacity: 0.85;
+      font-weight: normal;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
 }
 
 .discussion-body,
@@ -178,8 +190,25 @@
       padding-right: 5px;
     }
 
-    &:last-child {
-      padding-left: 5px;
+  }
+
+  .discussion-actions {
+    display: table;
+
+    .new-issue-for-discussion path {
+      fill: $gray-darkest;
+    }
+
+    .btn-group {
+      display: table-cell;
+
+      &:first-child {
+        padding-right: 0;
+      }
+
+      &:first-child:not(:last-child) > div {
+        border-right: 0;
+      }
     }
   }
 
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index aa130a1abb0d037890489a7f023ba90ec86bbff0..e238f0865f6961be2b78b8280179003df8e76e24 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -331,6 +331,10 @@ ul.notes {
 
     &:hover {
       color: $gl-link-color;
+    }
+
+    &:focus,
+    &:hover {
       text-decoration: none;
     }
   }
@@ -380,7 +384,7 @@ ul.notes {
   top: 0;
 
   .note-action-button {
-    margin-left: 10px;
+    margin-left: 8px;
   }
 }
 
@@ -396,8 +400,7 @@ ul.notes {
 }
 
 .note-action-button {
-  display: inline-block;
-  margin-left: 0;
+  display: inline;
   line-height: 20px;
 
   @media (min-width: $screen-sm-min) {
@@ -452,36 +455,37 @@ ul.notes {
  * Line note button on the side of diffs
  */
 
-.diff-file tr.line_holder {
-  @mixin show-add-diff-note {
-    display: inline-block;
-  }
+.add-diff-note {
+  display: none;
+  margin-top: -2px;
+  border-radius: 50%;
+  background: $white-light;
+  padding: 1px 5px;
+  font-size: 12px;
+  color: $gl-link-color;
+  margin-left: -55px;
+  position: absolute;
+  z-index: 10;
+  width: 23px;
+  height: 23px;
+  border: 1px solid $border-color;
+  transition: transform .1s ease-in-out;
 
-  .add-diff-note {
-    margin-top: -8px;
-    border-radius: 40px;
-    background: $white-light;
-    padding: 4px;
-    font-size: 16px;
-    color: $gl-link-color;
-    margin-left: -56px;
-    position: absolute;
-    z-index: 10;
-    width: 32px;
-    // "hide" it by default
-    display: none;
+  &:hover {
+    background: $gl-info;
+    color: $white-light;
+    transform: scale(1.15);
+  }
 
-    &:hover {
-      background: $gl-info;
-      color: $white-light;
-      @include show-add-diff-note;
-    }
+  &:active {
+    outline: 0;
   }
+}
 
-  // "show" the icon also if we just hover somewhere over the line
-  &:hover > td {
+.diff-file {
+  .is-over {
     .add-diff-note {
-      @include show-add-diff-note;
+      display: inline-block;
     }
   }
 }
@@ -505,6 +509,7 @@ ul.notes {
 }
 
 .line-resolve-all-container {
+
   .btn-group {
     margin-left: -4px;
   }
@@ -513,6 +518,27 @@ ul.notes {
     border-top-left-radius: 0;
     border-bottom-left-radius: 0;
   }
+
+  .btn.discussion-create-issue-btn {
+    margin-left: -4px;
+    border-radius: 0;
+    border-right: 0;
+
+    a {
+      padding: 0;
+      line-height: 0;
+
+      &:hover {
+        text-decoration: none;
+        border: 0;
+      }
+    }
+
+    .new-issue-for-discussion path {
+      fill: $gray-darkest;
+    }
+  }
+
 }
 
 .line-resolve-all {
@@ -535,7 +561,6 @@ ul.notes {
 }
 
 .line-resolve-btn {
-  display: inline-block;
   position: relative;
   top: 2px;
   padding: 0;
@@ -558,8 +583,9 @@ ul.notes {
   }
 
   svg {
-    position: relative;
     fill: $gray-darkest;
+    height: 15px;
+    width: 15px;
   }
 }
 
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3fe1eef307e3b4d1036be5bb2a0acae90b3f9d97..33b38ca692339d23d32aedffbf698ec5c6144212 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -13,21 +13,16 @@
     white-space: nowrap;
   }
 
-  .commit-title {
-    margin: 0;
-  }
-
-  .controls {
-    white-space: nowrap;
+  .table-holder {
+    width: 100%;
+    overflow: auto;
   }
 
-  .btn {
-    margin: 4px;
+  .commit-title {
+    margin: 0;
   }
 
   .table.ci-table {
-    min-width: 1200px;
-    table-layout: fixed;
 
     .label {
       margin-bottom: 3px;
@@ -37,16 +32,67 @@
       color: $black;
     }
 
-    .pipeline-date,
-    .pipeline-status {
-      width: 10%;
+    .stage-cell {
+      min-width: 130px; // Guarantees we show at least 4 stages in line
+      width: 20%;
+    }
+
+    .pipelines-time-ago {
+      text-align: right;
     }
 
-    .pipeline-info,
-    .pipeline-commit,
-    .pipeline-stages,
     .pipeline-actions {
-      width: 20%;
+      padding-right: 0;
+      min-width: 170px; //Guarantees buttons don't break in several lines.
+
+      .btn-default {
+        color: $gl-text-color-secondary;
+      }
+
+      .btn.btn-retry:hover,
+      .btn.btn-retry:focus {
+        border-color: $gray-darkest;
+        background-color: $white-normal;
+      }
+
+      svg path {
+        fill: $gl-text-color-secondary;
+      }
+
+      .dropdown-menu {
+        max-height: 250px;
+        overflow-y: auto;
+      }
+
+      .dropdown-toggle,
+      .dropdown-menu {
+        color: $gl-text-color-secondary;
+
+        .fa {
+          color: $gl-text-color-secondary;
+          font-size: 14px;
+        }
+      }
+
+      .btn-group {
+        &.open {
+          .btn-default {
+            background-color: $white-normal;
+            border-color: $border-white-normal;
+          }
+        }
+
+        .btn {
+          .icon-play {
+            height: 13px;
+            width: 12px;
+          }
+        }
+      }
+
+      .tooltip {
+        white-space: nowrap;
+      }
     }
   }
 }
@@ -54,6 +100,7 @@
 @media (max-width: $screen-md-max) {
   .content-list {
     &.pipelines,
+    &.environments-container,
     &.builds-content-list {
       width: 100%;
       overflow: auto;
@@ -61,27 +108,10 @@
   }
 }
 
-.content-list.pipelines .table-holder {
-  min-height: 300px;
-}
-
-.pipeline-holder {
-  width: 100%;
-  overflow: auto;
-}
-
 .table.ci-table {
-  min-width: 900px;
 
-  &.pipeline {
-    min-width: 650px;
-  }
-
-  &.builds-page {
-
-    tr {
-      height: 71px;
-    }
+  &.builds-page tbody tr {
+    height: 71px;
   }
 
   tr {
@@ -94,12 +124,16 @@
       padding: 10px 8px;
     }
 
+    td.environments-actions {
+      padding-right: 0;
+    }
+
     td.stage-cell {
       padding: 10px 0;
     }
 
     .commit-link {
-      padding: 9px 8px 10px;
+      padding: 9px 8px 10px 2px;
     }
   }
 
@@ -206,72 +240,8 @@
     }
   }
 
-  .pipeline-actions {
-    min-width: 140px;
-
-    .btn {
-      margin: 0;
-      color: $gl-text-color-secondary;
-    }
-
-    .cancel-retry-btns {
-      vertical-align: middle;
-
-      .btn:not(:first-child) {
-        margin-left: 8px;
-      }
-    }
-
-    .dropdown-menu {
-      max-height: 250px;
-      overflow-y: auto;
-    }
-
-    .dropdown-toggle,
-    .dropdown-menu {
-      color: $gl-text-color-secondary;
-
-      .fa {
-        color: $gl-text-color-secondary;
-        font-size: 14px;
-      }
-
-      svg,
-      .fa {
-        margin-right: 0;
-      }
-    }
-
-    .btn-remove {
-      color: $white-light;
-    }
-
-    .btn-group {
-      &.open {
-        .btn-default {
-          background-color: $white-normal;
-          border-color: $border-white-normal;
-        }
-      }
-
-      .btn {
-        .icon-play {
-          height: 13px;
-          width: 12px;
-        }
-      }
-    }
-
-    .tooltip {
-      white-space: nowrap;
-    }
-  }
-
-  .build-link {
-
-    a {
-      color: $gl-text-color;
-    }
+  .build-link a {
+    color: $gl-text-color;
   }
 
   .btn-group.open .dropdown-toggle {
@@ -335,31 +305,8 @@
 }
 
 .tab-pane {
-  &.pipelines {
-    .ci-table {
-      min-width: 900px;
-    }
-
-    .content-list.pipelines {
-      overflow: auto;
-    }
-
-    .stage {
-      max-width: 100px;
-      width: 100px;
-    }
-
-    .pipeline-actions {
-      min-width: initial;
-    }
-  }
-
-  &.builds {
-    .ci-table {
-      tr {
-        height: 71px;
-      }
-    }
+  &.builds .ci-table tr {
+    height: 71px;
   }
 }
 
@@ -969,3 +916,22 @@
     }
   }
 }
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+  border: none;
+  background: none;
+  outline: none;
+  width: 100%;
+  text-align: left;
+
+  .icon-play {
+    position: relative;
+    top: 2px;
+    margin-right: 5px;
+    height: 13px;
+    width: 12px;
+  }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8031c4467a47db0088728c4322f97e4e4ea7a370..1a983d8c9efc9e40d329827fd62ce04c651217d8 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -277,3 +277,42 @@ table.u2f-registrations {
     padding-left: 18px;
   }
 }
+
+.user-callout {
+  margin: 0 auto;
+
+  .bordered-box {
+    border: 1px solid $border-color;
+    border-radius: $border-radius-default;
+  }
+
+  .landing {
+    margin-top: $gl-padding;
+    margin-bottom: $gl-padding;
+
+    .close {
+      margin-right: 20px;
+    }
+
+    .dismiss-icon {
+      float: right;
+      cursor: pointer;
+      color: $cycle-analytics-dismiss-icon-color;
+    }
+
+    .svg-container {
+      text-align: center;
+
+      svg {
+        width: 136px;
+        height: 136px;
+      }
+    }
+  }
+
+  @media(max-width: $screen-xs-max) {
+    .inner-content {
+      padding-left: 30px;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8c0de314420c3eaf95db257ab9e888cc0ff5724d..efa47be9a7332bc890e1b83b203a5dcba95e9b31 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -494,11 +494,11 @@ a.deploy-project-label {
 .project-stats {
   font-size: 0;
   text-align: center;
-  border-bottom: 1px solid $border-color;
 
   .nav {
     padding-top: 12px;
     padding-bottom: 12px;
+    border-bottom: 1px solid $border-color;
   }
 
   .nav > li {
@@ -638,39 +638,22 @@ pre.light-well {
   margin: 0;
 }
 
-.activity-filter-block {
-  .controls {
-    padding-bottom: 7px;
-    margin-top: 8px;
-    border-bottom: 1px solid $border-color;
-  }
-}
-
 .commits-search-form {
   .input-short {
     min-width: 200px;
   }
 }
 
-.container-fluid.project-stats-container {
-  @media (max-width: $screen-xs-max) {
-    padding: 12px 0;
-  }
-}
-
 .project-last-commit {
   background-color: $gray-light;
-  padding: 12px $gl-padding;
   border: 1px solid $border-color;
+  border-radius: $border-radius-base;
+  padding: 12px;
 
   @media (min-width: $screen-sm-min) {
     margin-top: $gl-padding;
   }
 
-  @media (min-width: $screen-sm-min) {
-    border-radius: $border-radius-base;
-  }
-
   .ci-status {
     margin-right: $gl-padding;
   }
@@ -763,6 +746,8 @@ pre.light-well {
 }
 
 .protected-branches-list {
+  margin-bottom: 30px;
+
   a {
     color: $gl-text-color;
 
@@ -810,7 +795,8 @@ pre.light-well {
 }
 
 .project-refs-form .dropdown-menu,
-.dropdown-menu-projects {
+.dropdown-menu-projects,
+.dropdown-menu-branches {
   width: 300px;
 
   @media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 88ea92c5afb1e7e7d8184d984426cc3cb5a1cdfd..543d2ece3df54763f5082ee31071d650e1eab8d7 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -182,7 +182,8 @@ input[type="checkbox"]:hover {
     display: flex;
   }
 
-  .search-field-holder {
+  .search-field-holder,
+  .project-filter-form {
     -webkit-flex: 1 0 auto;
     flex: 1 0 auto;
     position: relative;
@@ -201,7 +202,8 @@ input[type="checkbox"]:hover {
     pointer-events: none;
   }
 
-  .search-text-input {
+  .search-text-input,
+  .project-filter-form-field {
     padding-left: $gl-padding + 15px;
     padding-right: $gl-padding + 15px;
   }
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a28a87ed4f8b16e0f97cf77c0f783800e9c4f367..3889deee21a5a4ee89a3996c050e674dd5632dd6 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,3 +24,14 @@
 .service-settings .control-label {
   padding-top: 0;
 }
+
+.token-token-container {
+  #impersonation-token-token {
+    width: 80%;
+    display: inline;
+  }
+
+  .btn-clipboard {
+    margin-left: 5px;
+  }
+}
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b97a29cd1a0cc4851107ff2bc3c5c984d9b8ab22
--- /dev/null
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -0,0 +1,12 @@
+.triggers-container {
+  .label-container {
+    display: inline-block;
+    margin-left: 10px;
+  }
+}
+
+.trigger-actions {
+  .btn {
+    margin-left: 10px;
+  }
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index af9ddb9ff807fe5548d17e542506644eabc10821..5f0aede4f5ed0a83b7065a463fd875804c2a8aea 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -170,7 +170,11 @@
 @media (max-width: $screen-sm-max) {
   .todos-filters {
     .dropdown-menu-toggle {
-      width: 135px;
+      width: 130px;
+    }
+
+    .dropdown-menu-toggle-sort {
+      width: auto;
     }
   }
 }
@@ -200,10 +204,6 @@
   }
 
   .todos-filters {
-    .row-content-block {
-      padding-bottom: 50px;
-    }
-
     .dropdown-menu-toggle {
       width: 100%;
     }
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e4487dbcb8723d4d468c1d38a707e9a445be9a54..fc4da4c495f077f9feb4517428d25f8c3894dd39 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -139,18 +139,10 @@
 .blob-commit-info {
   list-style: none;
   background: $gray-light;
-  padding: 6px 0;
+  padding: 16px 16px 16px 6px;
   border: 1px solid $border-color;
   border-bottom: none;
   margin: 0;
-
-  .table-list-cell {
-    border-bottom: none;
-  }
-
-  .commit-actions {
-    width: 260px;
-  }
 }
 
 #modal-remove-blob > .modal-dialog { width: 850px; }
@@ -178,3 +170,29 @@
     margin-left: $btn-side-margin;
   }
 }
+
+.repo-charts {
+  .sub-header {
+    margin: 20px 0;
+  }
+
+  .sub-header-block.border-top {
+    margin-top: 20px;
+    padding: 0;
+    border-top: 1px solid $white-dark;
+    border-bottom: none;
+  }
+
+  .commit-stats li {
+    font-size: 16px;
+  }
+
+  .tree-ref-header {
+    margin-bottom: 20px;
+
+    h4 {
+      margin: 0;
+      line-height: 36px;
+    }
+  }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index fb6df1a06d2a91047e6b912307b7147c52f9e7f0..1d0bd6e0b817ba8d9892863170d1bf0f196c5c87 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -89,6 +89,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
       :akismet_api_key,
       :akismet_enabled,
       :container_registry_token_expire_delay,
+      :default_artifacts_expire_in,
       :default_branch_protection,
       :default_group_visibility,
       :default_project_visibility,
@@ -143,6 +144,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
       :two_factor_grace_period,
       :user_default_external,
       :user_oauth_applications,
+      :unique_ips_limit_per_user,
+      :unique_ips_limit_time_window,
+      :unique_ips_limit_enabled,
       :version_check_enabled,
       :terminal_max_session_time,
 
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 62f62e99a97cfc1d668afbfeeb8d3e12cd682d33..9c9f420c1e0bf7e01ee694585ffccfd3b1a4eb07 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
   include OauthApplications
 
   before_action :set_application, only: [:show, :edit, :update, :destroy]
-  before_action :load_scopes, only: [:new, :edit]
+  before_action :load_scopes, only: [:new, :create, :edit, :update]
 
   def index
     @applications = Doorkeeper::Application.where("owner_id IS NULL")
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index 241c7be0ea14df6e4972ff1e5684ba3965aeca08..caf4c138da802f3071c6c3f5f579b949355269d2 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -1,5 +1,5 @@
 class Admin::HealthCheckController < Admin::ApplicationController
   def show
-    @errors = HealthCheck::Utils.process_checks('standard')
+    @errors = HealthCheck::Utils.process_checks(['standard'])
   end
 end
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..07c8bf714fcb2f31a60356856a7cb5814512e833
--- /dev/null
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -0,0 +1,53 @@
+class Admin::ImpersonationTokensController < Admin::ApplicationController
+  before_action :user
+
+  def index
+    set_index_vars
+  end
+
+  def create
+    @impersonation_token = finder.build(impersonation_token_params)
+
+    if @impersonation_token.save
+      flash[:impersonation_token] = @impersonation_token.token
+      redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
+    else
+      set_index_vars
+      render :index
+    end
+  end
+
+  def revoke
+    @impersonation_token = finder.find(params[:id])
+
+    if @impersonation_token.revoke!
+      flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!"
+    else
+      flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}."
+    end
+
+    redirect_to admin_user_impersonation_tokens_path
+  end
+
+  private
+
+  def user
+    @user ||= User.find_by!(username: params[:user_id])
+  end
+
+  def finder(options = {})
+    PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
+  end
+
+  def impersonation_token_params
+    params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
+  end
+
+  def set_index_vars
+    @scopes = Gitlab::Auth::API_SCOPES
+
+    @impersonation_token ||= finder.build
+    @inactive_impersonation_tokens = finder(state: 'inactive').execute
+    @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
+  end
+end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 39c8c6d8a0c0c216fbc2bf776dd03f1debdd4642..daecfc832bf7d82ddbfec3fe3972577881f73c55 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController
     @projects = @projects.search(params[:name]) if params[:name].present?
     @projects = @projects.sort(@sort = params[:sort])
     @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: {
+          html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects })
+        }
+      end
+    end
   end
 
   def show
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index 1330399a8366b787cb3ed2e876a8f38c84558769..990397245216f8a425d68932a21884ba7b6cdd53 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -3,7 +3,7 @@ class Admin::SystemInfoController < Admin::ApplicationController
     'nobrowse',
     'read-only',
     'ro'
-  ]
+  ].freeze
 
   EXCLUDED_MOUNT_TYPES = [
     'autofs',
@@ -27,7 +27,7 @@ class Admin::SystemInfoController < Admin::ApplicationController
     'tmpfs',
     'tracefs',
     'vfat'
-  ]
+  ].freeze
 
   def show
     @cpus = Vmstat.cpu rescue nil
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 7ffde71c3b15118a2e093ce15951337ad0c6dede..24504685e48a8689c4fcbf293de51dceedb4a260 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController
   end
 
   def impersonate
-    if user.blocked?
-      flash[:alert] = "You cannot impersonate a blocked user"
-
-      redirect_to admin_user_path(user)
-    else
+    if can?(user, :log_in)
       session[:impersonator_id] = current_user.id
 
       warden.set_user(user, scope: :user)
@@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController
       flash[:alert] = "You are now impersonating #{user.username}"
 
       redirect_to root_path
+    else
+      flash[:alert] =
+        if user.blocked?
+          "You cannot impersonate a blocked user"
+        elsif user.internal?
+          "You cannot impersonate an internal user"
+        else
+          "You cannot impersonate a user who cannot log in"
+        end
+
+      redirect_to admin_user_path(user)
     end
   end
 
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 5e7af3bff0df919e762b25455e0b58c1764829aa..b7ce081a5cde3d3ea743b01ac0f77f142b4921cf 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
     render_403
   end
 
+  rescue_from Gitlab::Auth::TooManyIps do |e|
+    head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window
+  end
+
   def redirect_back_or_default(default: root_path, options: {})
     redirect_to request.referer.present? ? :back : default, options
   end
@@ -63,7 +67,7 @@ class ApplicationController < ActionController::Base
     token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
     user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
 
-    if user
+    if user && can?(user, :log_in)
       # Notice we are passing store false, so the user is not
       # actually stored in the session and a token is needed
       # for every request. If you want the token to work as a
@@ -72,14 +76,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def authenticate_user!(*args)
-    if redirect_to_home_page_url?
-      return redirect_to current_application_settings.home_page_url
-    end
-
-    super(*args)
-  end
-
   def log_exception(exception)
     application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
     application_trace.map!{ |t| "  #{t}\n" }
@@ -94,7 +90,7 @@ class ApplicationController < ActionController::Base
     current_application_settings.after_sign_out_path.presence || new_user_session_path
   end
 
-  def can?(object, action, subject)
+  def can?(object, action, subject = :global)
     Ability.allowed?(object, action, subject)
   end
 
@@ -130,10 +126,6 @@ class ApplicationController < ActionController::Base
     headers['X-XSS-Protection'] = '1; mode=block'
     headers['X-UA-Compatible'] = 'IE=edge'
     headers['X-Content-Type-Options'] = 'nosniff'
-    # Enabling HSTS for non-standard ports would send clients to the wrong port
-    if Gitlab.config.gitlab.https && Gitlab.config.gitlab.port == 443
-      headers['Strict-Transport-Security'] = 'max-age=31536000'
-    end
   end
 
   def validate_user_service_ticket!
@@ -181,7 +173,7 @@ class ApplicationController < ActionController::Base
   end
 
   def gitlab_ldap_access(&block)
-    Gitlab::LDAP::Access.open { |access| block.call(access) }
+    Gitlab::LDAP::Access.open { |access| yield(access) }
   end
 
   # JSON for infinite scroll via Pager object
@@ -287,19 +279,6 @@ class ApplicationController < ActionController::Base
     session[:skip_tfa] && session[:skip_tfa] > Time.current
   end
 
-  def redirect_to_home_page_url?
-    # If user is not signed-in and tries to access root_path - redirect him to landing page
-    # Don't redirect to the default URL to prevent endless redirections
-    return false unless current_application_settings.home_page_url.present?
-
-    home_page_url = current_application_settings.home_page_url.chomp('/')
-    root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
-
-    return false if root_urls.include?(home_page_url)
-
-    current_user.nil? && root_path == request.path
-  end
-
   # U2F (universal 2nd factor) devices need a unique identifier for the application
   # to perform authentication.
   # https://developers.yubico.com/U2F/App_ID.html
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d7a45bacd3573b1bf86c5397e7cbf7c8cf340312..b79ca034c5bbcda2a897e424c2a6fa2faa58c8c8 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController
     if params[:search].blank?
       # Include current user if available to filter by "Me"
       if params[:current_user].present? && current_user
-        @users = @users.where.not(id: current_user.id)
-        @users = [current_user, *@users]
+        @users = [current_user, *@users].uniq
       end
 
       if params[:author_id].present?
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
deleted file mode 100644
index ff297d6ff13143b1b0f762e056eb67d090d72161..0000000000000000000000000000000000000000
--- a/app/controllers/ci/projects_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Ci
-  class ProjectsController < ::ApplicationController
-    before_action :project
-    before_action :no_cache, only: [:badge]
-    before_action :authorize_read_project!, except: [:badge, :index]
-    skip_before_action :authenticate_user!, only: [:badge]
-    protect_from_forgery
-
-    def index
-      redirect_to root_path
-    end
-
-    def show
-      # Temporary compatibility with CI badges pointing to CI project page
-      redirect_to namespace_project_path(project.namespace, project)
-    end
-
-    # Project status badge
-    # Image with build status for sha or ref
-    #
-    # This action in DEPRECATED, this is here only for backwards compatibility
-    # with projects migrated from GitLab CI.
-    #
-    def badge
-      return render_404 unless @project
-
-      image = Ci::ImageForBuildService.new.execute(@project, params)
-      send_file image.path, filename: image.name, disposition: 'inline', type: "image/svg+xml"
-    end
-
-    protected
-
-    def project
-      @project ||= Project.find_by(ci_id: params[:id].to_i)
-    end
-
-    def no_cache
-      response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
-      response.headers["Pragma"] = "no-cache"
-      response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
-    end
-
-    def authorize_read_project!
-      return access_denied! unless can?(current_user, :read_project, project)
-    end
-  end
-end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 4c497711fc00a1710e586193ba2083b65dc52b65..ea441b1736b6518bbd6eca505092f53acbcf447a 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor
   #
   # Returns nil
   def prompt_for_two_factor(user)
-    return locked_user_redirect(user) if user.access_locked?
+    return locked_user_redirect(user) unless user.can?(:log_in)
 
     session[:otp_user_id] = user.id
     setup_u2f_authentication(user)
@@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor
 
   def authenticate_with_two_factor
     user = self.resource = find_user
+    return locked_user_redirect(user) unless user.can?(:log_in)
 
-    if user.access_locked?
-      locked_user_redirect(user)
-    elsif user_params[:otp_attempt].present? && session[:otp_user_id]
+    if user_params[:otp_attempt].present? && session[:otp_user_id]
       authenticate_with_two_factor_via_otp(user)
     elsif user_params[:device_response].present? && session[:otp_user_id]
       authenticate_with_two_factor_via_u2f(user)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 88d180fcc2ececeead35e4dda54edcb11f9c69d9..9ac8197e45a22c050f64147044c5e881eedc65e0 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -4,10 +4,9 @@ module CreatesCommit
   def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
     set_commit_variables
 
-    start_branch = @mr_target_branch unless initial_commit?
     commit_params = @commit_params.merge(
       start_project: @mr_target_project,
-      start_branch: start_branch,
+      start_branch: @mr_target_branch,
       target_branch: @mr_source_branch
     )
 
@@ -17,12 +16,16 @@ module CreatesCommit
     if result[:status] == :success
       update_flash_notice(success_notice)
 
+      success_path = final_success_path(success_path)
+
       respond_to do |format|
-        format.html { redirect_to final_success_path(success_path) }
-        format.json { render json: { message: "success", filePath: final_success_path(success_path) } }
+        format.html { redirect_to success_path }
+        format.json { render json: { message: "success", filePath: success_path } }
       end
     else
       flash[:alert] = result[:message]
+      failure_path = failure_path.call if failure_path.respond_to?(:call)
+
       respond_to do |format|
         format.html do
           if failure_view
@@ -58,9 +61,13 @@ module CreatesCommit
   end
 
   def final_success_path(success_path)
-    return success_path unless create_merge_request?
+    if create_merge_request?
+      merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+    else
+      success_path = success_path.call if success_path.respond_to?(:call)
 
-    merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+      success_path
+    end
   end
 
   def new_merge_request_path
@@ -92,46 +99,26 @@ module CreatesCommit
   end
 
   def create_merge_request?
-    # XXX: Even if the field is set, if we're checking the same branch
+    # Even if the field is set, if we're checking the same branch
     # as the target branch in the same project,
     # we don't want to create a merge request.
     params[:create_merge_request].present? &&
-      (different_project? || @ref != @target_branch)
+      (different_project? || @mr_target_branch != @mr_source_branch)
   end
 
-  # TODO: We should really clean this up
   def set_commit_variables
     if can?(current_user, :push_code, @project)
-      # Edit file in this project
       @mr_source_project = @project
+      @target_branch ||= @ref
     else
-      # Merge request from fork to this project
       @mr_source_project = current_user.fork_of(@project)
+      @target_branch ||= @mr_source_project.repository.next_branch('patch')
     end
 
     # Merge request to this project
     @mr_target_project = @project
-    @mr_target_branch = @ref || @target_branch
-
-    @mr_source_branch = guess_mr_source_branch
-  end
-
-  def initial_commit?
-    @mr_target_branch.nil? ||
-      !@mr_target_project.repository.branch_exists?(@mr_target_branch)
-  end
+    @mr_target_branch ||= @ref || @target_branch
 
-  def guess_mr_source_branch
-    # XXX: Happens when viewing a commit without a branch. In this case,
-    # @target_branch would be the default branch for @mr_source_project,
-    # however we want a generated new branch here. Thus we can't use
-    # @target_branch, but should pass nil to indicate that we want a new
-    # branch instead of @target_branch.
-    return if
-      create_merge_request? &&
-          # XXX: Don't understand why rubocop prefers this indention
-          @mr_source_project.repository.branch_exists?(@target_branch)
-
-    @target_branch
+    @mr_source_branch = @target_branch
   end
 end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
index 586f97c5eb47bebf955ebf01693d9be19bacb3a1..6014112256ab9f1d23778c0dd0198ceb1358489b 100644
--- a/app/controllers/concerns/filter_projects.rb
+++ b/app/controllers/concerns/filter_projects.rb
@@ -8,7 +8,7 @@ module FilterProjects
   extend ActiveSupport::Concern
 
   def filter_projects(projects)
-    projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
+    projects = projects.search(params[:name]) if params[:name].present?
     projects = projects.non_archived if params[:archived].blank?
     projects = projects.personal(current_user) if params[:personal].present? && current_user
 
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0821974aa93b7a96469b007ab2802060012eb3ac..3ccf2a9ce33e12acd0cfd3cf847b546abef9c357 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -26,6 +26,23 @@ module IssuableActions
 
   private
 
+  def render_conflict_response
+    respond_to do |format|
+      format.html do
+        @conflict = true
+        render :edit
+      end
+
+      format.json do
+        render json: {
+          errors: [
+            "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
+          ]
+        }, status: 409
+      end
+    end
+  end
+
   def labels
     @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
   end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index a6e158ebae6a7474e260ae132a5dbdeba5f675f6..85ae4985e58cf247a001d158ea80c262eadb97eb 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,24 +9,32 @@ module IssuableCollections
 
   private
 
-  def issuable_meta_data(issuable_collection)
+  def issuable_meta_data(issuable_collection, collection_type)
     # map has to be used here since using pluck or select will
     # throw an error when ordering issuables by priority which inserts
     # a new order into the collection.
     # We cannot use reorder to not mess up the paginated collection.
-    issuable_ids         = issuable_collection.map(&:id)
-    issuable_note_count  = Note.count_for_collection(issuable_ids, @collection_type)
+    issuable_ids = issuable_collection.map(&:id)
+    issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
     issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
+    issuable_merge_requests_count =
+      if collection_type == 'Issue'
+        MergeRequestsClosingIssues.count_for_collection(issuable_ids)
+      else
+        []
+      end
 
     issuable_ids.each_with_object({}) do |id, issuable_meta|
       downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
-      upvotes   = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
-      notes     = issuable_note_count.find  { |notes| notes.noteable_id == id }
+      upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
+      notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+      merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
 
       issuable_meta[id] = Issuable::IssuableMeta.new(
         upvotes.try(:count).to_i,
         downvotes.try(:count).to_i,
-        notes.try(:count).to_i
+        notes.try(:count).to_i,
+        merge_requests.try(:last).to_i
       )
     end
   end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index fb5edb343706c243fdbba2c0885c64341600940f..b17c138d5c74d4c154df6111108f902ce44cb89e 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -10,7 +10,7 @@ module IssuesAction
               .page(params[:page])
 
     @collection_type    = "Issue"
-    @issuable_meta_data = issuable_meta_data(@issues)
+    @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
 
     respond_to do |format|
       format.html
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 6229759dcf139482c7995a0d0ecb23597dd4a4e8..d3c8e4888bca3bcbc87bf7b08b6cf2981e7229a5 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -9,7 +9,7 @@ module MergeRequestsAction
                       .page(params[:page])
 
     @collection_type    = "MergeRequest"
-    @issuable_meta_data = issuable_meta_data(@merge_requests)
+    @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
   end
 
   private
diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0854c73a02fec2df6526d9c3ad09525c20ff8a90
--- /dev/null
+++ b/app/controllers/concerns/repository_settings_redirect.rb
@@ -0,0 +1,7 @@
+module RepositorySettingsRedirect
+  extend ActiveSupport::Concern
+
+  def redirect_to_repository_settings(project)
+    redirect_to namespace_project_settings_repository_path(project.namespace, project)
+  end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index d7f5a4e46829810a1cbb200e5374d32f56375fa0..a8c0937569c3f0fb152e9b3cadc648fa162a2b6a 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -33,10 +33,10 @@ module ServiceParams
     :issues_url,
     :jira_issue_transition_id,
     :merge_requests_events,
+    :mock_service_url,
     :namespace,
     :new_issue_url,
     :notify,
-    :notify_only_broken_builds,
     :notify_only_broken_pipelines,
     :password,
     :priority,
@@ -59,10 +59,10 @@ module ServiceParams
     :user_key,
     :username,
     :webhook
-  ]
+  ].freeze
 
   # Parameters to ignore if no value is specified
-  FILTER_BLANK_PARAMS = [:password]
+  FILTER_BLANK_PARAMS = [:password].freeze
 
   def service_params
     dynamic_params = @service.event_channel_names + @service.event_names
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index da225d8f1c75cb5fade735f9a1e211817ff4e779..d0a692070d93ec5ed04c833abd987f934d8e225a 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -27,7 +27,7 @@ module SpammableActions
 
       render :verify
     else
-      fallback.call
+      yield
     end
   end
 
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 0b7cf8167f017453175a9ebed093e77cff770151..d03265e9f20670699c33dfb5b3a0780e79270d60 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,17 @@
 class Dashboard::GroupsController < Dashboard::ApplicationController
   def index
-    @group_members = current_user.group_members.includes(source: :route).page(params[:page])
+    @group_members = current_user.group_members.includes(source: :route).joins(:group)
+    @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
+    @group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
+    @group_members = @group_members.page(params[:page])
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: {
+          html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
+        }
+      end
+    end
   end
 end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 7f506db583f5d36f3a5cafa5241c6e9b793ca0c1..df528d10f6ef7fbcd5a9c0a47a4afff5c5e4b74d 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
   def index
     respond_to do |format|
       format.html do
+        @milestone_states = GlobalMilestone.states_count(@projects)
         @milestones = Kaminari.paginate_array(milestones).page(params[:page])
       end
       format.json do
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 325ae56553759ce9bc8b68de5ff6bf9cefaf6e9e..be00d765f73a578a09453d42168fa91455791cd6 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -42,7 +42,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
   private
 
   def load_projects(base_scope)
-    projects = base_scope.sorted_by_activity.includes(:namespace)
+    projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
 
     filter_projects(projects)
   end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 5848ca62777c8d5dd4c520d1fdfa980f0480ea12..096de8032ae75f3eb864723a468c0b97d61f2f70 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
   end
 
   def destroy_all
-    TodoService.new.mark_todos_as_done(@todos, current_user)
+    updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
 
     respond_to do |format|
       format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
       format.js { head :ok }
-      format.json { render json: todos_counts }
+      format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
     end
   end
 
@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
     render json: todos_counts
   end
 
+  def bulk_restore
+    TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
+
+    render json: todos_counts
+  end
+
   # Used in TodosHelper also
   def self.todos_count_format(count)
     count >= 100 ? '99+' : count
@@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
   private
 
   def find_todos
-    @todos ||= TodosFinder.new(current_user, params).execute
+    @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
   end
 
   def todos_counts
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
deleted file mode 100644
index 1bec5a7d27fffa82ec172648727576a54fbbc45f..0000000000000000000000000000000000000000
--- a/app/controllers/emojis_controller.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class EmojisController < ApplicationController
-  layout false
-
-  def index
-  end
-end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index a962f9a0937f16b4dd08aaef60c1acd19ebc4c85..68228c095dafba27cc9dd90b78538127825d2643 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,8 +1,17 @@
 class Explore::GroupsController < Explore::ApplicationController
   def index
     @groups = GroupsFinder.new.execute(current_user)
-    @groups = @groups.search(params[:search]) if params[:search].present?
+    @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
     @groups = @groups.sort(@sort = params[:sort])
     @groups = @groups.page(params[:page])
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: {
+          html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups })
+        }
+      end
+    end
   end
 end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 26e17a7553ef616d1fb4aa06ddfac1ad45803758..6167f9bd3359882b0adef74832902e812db3a57e 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -2,7 +2,7 @@ class Explore::ProjectsController < Explore::ApplicationController
   include FilterProjects
 
   def index
-    @projects = ProjectsFinder.new.execute(current_user)
+    @projects = load_projects
     @tags = @projects.tags_on(:tags)
     @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
     @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@@ -21,7 +21,8 @@ class Explore::ProjectsController < Explore::ApplicationController
   end
 
   def trending
-    @projects = filter_projects(Project.trending)
+    @projects = load_projects(Project.trending)
+    @projects = filter_projects(@projects)
     @projects = @projects.sort(@sort = params[:sort])
     @projects = @projects.page(params[:page])
 
@@ -36,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController
   end
 
   def starred
-    @projects = ProjectsFinder.new.execute(current_user)
+    @projects = load_projects
     @projects = filter_projects(@projects)
     @projects = @projects.reorder('star_count DESC')
     @projects = @projects.page(params[:page])
@@ -50,4 +51,11 @@ class Explore::ProjectsController < Explore::ApplicationController
       end
     end
   end
+
+  protected
+
+  def load_projects(base_scope = nil)
+    base_scope ||= ProjectsFinder.new.execute(current_user)
+    base_scope.includes(:route, namespace: :route)
+  end
 end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0d872c86c8a7942496f72984b7648a36dd18bced..4310259620151e1f61f66f74e9a4af34b9dec675 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController
   def index
     respond_to do |format|
       format.html do
+        @milestone_states = GlobalMilestone.states_count(@projects)
         @milestones = Kaminari.paginate_array(milestones).page(params[:page])
       end
     end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 7ed54479599d2cdfb16b9376af710458e638261f..05f9ee1ee90e570d5d2e9d3e7562bc9f4e3a05de 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController
     @group = Groups::CreateService.new(current_user, group_params).execute
 
     if @group.persisted?
-      redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
+      notice = if @group.chat_team.present?
+                 "Group '#{@group.name}' and its Mattermost team were successfully created."
+               else
+                 "Group '#{@group.name}' was successfully created."
+               end
+
+      redirect_to @group, notice: notice
     else
       render action: "new"
     end
@@ -108,11 +114,11 @@ class GroupsController < Groups::ApplicationController
     @projects = @projects.sorted_by_activity
     @projects = filter_projects(@projects)
     @projects = @projects.sort(@sort = params[:sort])
-    @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
+    @projects = @projects.page(params[:page]) if params[:name].blank?
   end
 
   def authorize_create_group!
-    unless can?(current_user, :create_group, nil)
+    unless can?(current_user, :create_group)
       return render_404
     end
   end
@@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController
       :request_access_enabled,
       :share_with_group_lock,
       :visibility_level,
-      :parent_id
+      :parent_id,
+      :create_chat_team,
+      :chat_team_name
     ]
   end
 
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c2e4d62b50be10d5f7ed700c72b070dd3200f3a2..3109439b2ff4853e84ea9733100fdbd154d41933 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -5,7 +5,7 @@ class JwtController < ApplicationController
 
   SERVICES = {
     Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService,
-  }
+  }.freeze
 
   def auth
     service = SERVICES[params[:service]]
@@ -39,7 +39,8 @@ class JwtController < ApplicationController
           message: "HTTP Basic: Access denied\n" \
                    "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
                    "You can generate one at #{profile_personal_access_tokens_url}" }
-      ] }, status: 401
+      ]
+    }, status: 401
   end
 
   def render_unauthorized
@@ -47,7 +48,8 @@ class JwtController < ApplicationController
       errors: [
         { code: 'UNAUTHORIZED',
           message: 'HTTP Basic: Access denied' }
-      ] }, status: 401
+      ]
+    }, status: 401
   end
 
   def auth_params
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index c721dca58d93f0133c45e5264cbc98c310e99198..05190103767077e5b6a93b2aa211440c91ba45b3 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,8 +1,8 @@
 class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
-  before_action :authenticate_resource_owner!
-
   layout 'profile'
 
+  # Overriden from Doorkeeper::AuthorizationsController to
+  # include the call to session.delete
   def new
     if pre_auth.authorizable?
       if skip_authorization? || matching_token?
@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
       render "doorkeeper/authorizations/error"
     end
   end
-
-  # TODO: Handle raise invalid authorization
-  def create
-    redirect_or_render authorization.authorize
-  end
-
-  def destroy
-    redirect_or_render authorization.deny
-  end
-
-  private
-
-  def matching_token?
-    Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
-                                               current_resource_owner.id,
-                                               pre_auth.scopes)
-  end
-
-  def redirect_or_render(auth)
-    if auth.redirectable?
-      redirect_to auth.redirect_uri
-    else
-      render json: auth.body, status: auth.status
-    end
-  end
-
-  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
 end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index c8663a3c38ee271d4080364e7b54223f1e19825d..e4452f46056bf21781f31040206b53668484e8a8 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -10,11 +10,6 @@ class Profiles::KeysController < Profiles::ApplicationController
     @key = current_user.keys.find(params[:id])
   end
 
-  # Back-compat: We need to support this URL since git-annex webapp points to it
-  def new
-    redirect_to profile_keys_path
-  end
-
   def create
     @key = current_user.keys.new(key_params)
 
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index a271e2dfc4ba8a61161fe4cf65b4d079421251a4..b8b71d295f6d2757d3aa9da3417df86411a258f4 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
   end
 
   def user_params
-    params.require(:user).permit(:notification_email, :notified_of_own_activity)
+    params.require(:user).permit(:notification_email)
   end
 end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 6e007f17913350b0f92955218ffc2f93a4f2c25b..0abe7ea3c9bc0d5102cfa9d51079bb50422a4df4 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
   end
 
   def create
-    @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+    @personal_access_token = finder.build(personal_access_token_params)
 
     if @personal_access_token.save
       flash[:personal_access_token] = @personal_access_token.token
@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
   end
 
   def revoke
-    @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+    @personal_access_token = finder.find(params[:id])
 
     if @personal_access_token.revoke!
       flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
 
   private
 
+  def finder(options = {})
+    PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options))
+  end
+
   def personal_access_token_params
     params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
   end
 
   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
+    @scopes = Gitlab::Auth::API_SCOPES
+
+    @personal_access_token = finder.build
+    @inactive_personal_access_tokens = finder(state: 'inactive').execute
+    @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
   end
 end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 18044ca78e2074285027ce2ba6babec795d786ca..26e7e93533ef84b995c1a23e8696ac87b64cf1f8 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -80,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
 
   def build_qr_code
     uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
-    RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
+    RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
   end
 
   def account_string
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index f0c71725ea8c851a66c38bc209472f9657f8fa39..987b95e89b9e6ead5657ccdddd07a96888d155a7 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController
   end
 
   def update_username
-    @user.update_attributes(username: user_params[:username])
-
-    respond_to do |format|
-      format.js
+    if @user.update_attributes(username: user_params[:username])
+      options = { notice: "Username successfully changed" }
+    else
+      message = @user.errors.full_messages.uniq.join('. ')
+      options = { alert: "Username change failed - #{message}" }
     end
+
+    redirect_back_or_default(default: { action: 'show' }, options: options)
   end
 
   private
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index d9dfa53466981896e96e495d295fff7452934cda..ffb54390965a6c82b004aa5c7a2fc3da3c1e4a23 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,9 +1,5 @@
 class Projects::AutocompleteSourcesController < Projects::ApplicationController
-  before_action :load_autocomplete_service, except: [:emojis, :members]
-
-  def emojis
-    render json: Gitlab::AwardEmoji.urls
-  end
+  before_action :load_autocomplete_service, except: [:members]
 
   def members
     render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 863a766a2552cb3ee18e11d4c03c712cfb8a19de..6461eeac11c7293d2ea616c6ebfab3cb6738eb45 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController
 
   def show
     @blob = @repository.blob_at(@commit.id, @path)
-    
+
     return render_404 unless @blob
 
+    environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+    @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
     @blame_groups = Gitlab::Blame.new(@blob, @commit).groups
   end
 end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 39ba815cfcaf17c0bf87ba33d0bc7cb471ed50ee..52fc67d162c7f35ae83d35348984ab9fe68e7045 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -5,7 +5,7 @@ class Projects::BlobController < Projects::ApplicationController
   include ActionView::Helpers::SanitizeHelper
 
   # Raised when given an invalid file path
-  class InvalidPathError < StandardError; end
+  InvalidPathError = Class.new(StandardError)
 
   before_action :require_non_empty_project, except: [:new, :create]
   before_action :authorize_download_code!
@@ -23,8 +23,10 @@ class Projects::BlobController < Projects::ApplicationController
   end
 
   def create
+    update_ref
+
     create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
-                                        success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)),
+                                        success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
                                         failure_view: :new,
                                         failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
   end
@@ -40,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController
 
   def update
     @path = params[:file_path] if params[:file_path].present?
-    create_commit(Files::UpdateService, success_path: after_edit_path,
+    create_commit(Files::UpdateService, success_path: -> { after_edit_path },
                                         failure_view: :edit,
                                         failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
 
@@ -62,7 +64,7 @@ class Projects::BlobController < Projects::ApplicationController
 
   def destroy
     create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
-                                         success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
+                                         success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) },
                                          failure_view: :show,
                                          failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
   end
@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
 
   private
 
+  def update_ref
+    branch_exists = @repository.find_branch(@target_branch)
+    @ref = @target_branch if branch_exists
+  end
+
   def blob
     @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
 
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 61fef4dc133d183c61d84e8dce55f813c675922b..28c9646910d2cffdae0e1c0781e22c5597aad06d 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -8,6 +8,7 @@ module Projects
       def index
         issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
         issues = issues.page(params[:page]).per(params[:per] || 20)
+        make_sure_position_is_set(issues)
 
         render json: {
           issues: serialize_as_json(issues),
@@ -38,6 +39,12 @@ module Projects
 
       private
 
+      def make_sure_position_is_set(issues)
+        issues.each do |issue|
+          issue.move_to_end && issue.save unless issue.relative_position
+        end
+      end
+
       def issue
         @issue ||=
           IssuesFinder.new(current_user, project_id: project.id)
@@ -63,7 +70,7 @@ module Projects
       end
 
       def move_params
-        params.permit(:board_id, :id, :from_list_id, :to_list_id)
+        params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
       end
 
       def issue_params
@@ -73,7 +80,7 @@ module Projects
       def serialize_as_json(resource)
         resource.as_json(
           labels: true,
-          only: [:id, :iid, :title, :confidential, :due_date],
+          only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
           include: {
             assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
             milestone: { only: [:id, :title] }
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 89d84809e3abf5dd5e85a6d35c1180b5407026e6..840405f38cbbbc5631edbf6f993167bb626232f7 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,25 +1,29 @@
 class Projects::BranchesController < Projects::ApplicationController
   include ActionView::Helpers::SanitizeHelper
   include SortingHelper
+
   # Authorize
-  before_action :require_non_empty_project
+  before_action :require_non_empty_project, except: :create
   before_action :authorize_download_code!
   before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
 
   def index
     @sort = params[:sort].presence || sort_value_name
     @branches = BranchesFinder.new(@repository, params).execute
-    @branches = Kaminari.paginate_array(@branches).page(params[:page])
-
-    @max_commits = @branches.reduce(0) do |memo, branch|
-      diverging_commit_counts = repository.diverging_commit_counts(branch)
-      [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
-    end
 
     respond_to do |format|
-      format.html
+      format.html do
+        paginate_branches
+        @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+
+        @max_commits = @branches.reduce(0) do |memo, branch|
+          diverging_commit_counts = repository.diverging_commit_counts(branch)
+          [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+        end
+      end
       format.json do
-        render json: @repository.branch_names
+        paginate_branches unless params[:show_all]
+        render json: @branches.map(&:name)
       end
     end
   end
@@ -32,6 +36,8 @@ class Projects::BranchesController < Projects::ApplicationController
     branch_name = sanitize(strip_tags(params[:branch_name]))
     branch_name = Addressable::URI.unescape(branch_name)
 
+    redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+
     result = CreateBranchService.new(project, current_user).
         execute(branch_name, ref)
 
@@ -42,8 +48,15 @@ class Projects::BranchesController < Projects::ApplicationController
 
     if result[:status] == :success
       @branch = result[:branch]
-      redirect_to namespace_project_tree_path(@project.namespace, @project,
-                                              @branch.name)
+
+      if redirect_to_autodeploy
+        redirect_to(
+          url_to_autodeploy_setup(project, branch_name),
+          notice: view_context.autodeploy_flash_notice(branch_name))
+      else
+        redirect_to namespace_project_tree_path(@project.namespace, @project,
+                                                @branch.name)
+      end
     else
       @error = result[:message]
       render action: 'new'
@@ -76,7 +89,23 @@ class Projects::BranchesController < Projects::ApplicationController
       ref_escaped = sanitize(strip_tags(params[:ref]))
       Addressable::URI.unescape(ref_escaped)
     else
-      @project.default_branch
+      @project.default_branch || 'master'
     end
   end
+
+  def paginate_branches
+    @branches = Kaminari.paginate_array(@branches).page(params[:page])
+  end
+
+  def url_to_autodeploy_setup(project, branch_name)
+    namespace_project_new_blob_path(
+      project.namespace,
+      project,
+      branch_name,
+      file_name: '.gitlab-ci.yml',
+      commit_message: 'Set up auto deploy',
+      target_branch: branch_name,
+      context: 'autodeploy'
+    )
+  end
 end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index e10d7992db754bea7122daa3045055c22859b5cf..cc67f688d51ddb20c9d0ab2bbe72e5dea4bef5eb 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -51,23 +51,35 @@ class Projects::CommitController < Projects::ApplicationController
   def revert
     assign_change_commit_vars
 
-    return render_404 if @target_branch.blank?
+    return render_404 if @start_branch.blank?
+
+    @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch
+
+    @mr_target_branch = @start_branch
 
     create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
-                                          success_path: successful_change_path, failure_path: failed_change_path)
+                                          success_path: -> { successful_change_path }, failure_path: failed_change_path)
   end
 
   def cherry_pick
     assign_change_commit_vars
 
-    return render_404 if @target_branch.blank?
+    return render_404 if @start_branch.blank?
+
+    @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
+
+    @mr_target_branch = @start_branch
 
     create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
-                                              success_path: successful_change_path, failure_path: failed_change_path)
+                                              success_path: -> { successful_change_path }, failure_path: failed_change_path)
   end
 
   private
 
+  def create_new_branch?
+    params[:create_merge_request].present? || !can?(current_user, :push_code, @project)
+  end
+
   def successful_change_path
     referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
   end
@@ -78,7 +90,7 @@ class Projects::CommitController < Projects::ApplicationController
 
   def referenced_merge_request_url
     if merge_request = @commit.merged_merge_request(current_user)
-      namespace_project_merge_request_url(@project.namespace, @project, merge_request)
+      namespace_project_merge_request_url(merge_request.target_project.namespace, merge_request.target_project, merge_request)
     end
   end
 
@@ -94,7 +106,7 @@ class Projects::CommitController < Projects::ApplicationController
 
     @diffs = commit.diffs(opts)
     @notes_count = commit.notes.count
-    
+
     @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
   end
 
@@ -118,11 +130,7 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def assign_change_commit_vars
-    @commit = project.commit(params[:id])
-    @target_branch = params[:target_branch]
-    @commit_params = {
-      commit: @commit,
-      create_merge_request: params[:create_merge_request].present? || different_project?
-    }
+    @start_branch = params[:start_branch]
+    @commit_params = { commit: @commit }
   end
 end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index b094491e006705d9a724613932c787eca57f99c4..1502b734f37e3845b0f8cab56e498141aea872c1 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -1,4 +1,5 @@
 class Projects::DeployKeysController < Projects::ApplicationController
+  include RepositorySettingsRedirect
   respond_to :html
 
   # Authorize
@@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController
   layout "project_settings"
 
   def index
-    @key = DeployKey.new
-    set_index_vars
+    redirect_to_repository_settings(@project)
   end
 
   def new
-    redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+    redirect_to_repository_settings(@project)
   end
 
   def create
     @key = DeployKey.new(deploy_key_params.merge(user: current_user))
-    set_index_vars
 
-    if @key.valid? && @project.deploy_keys << @key
-      redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
-    else
-      render "index"
+    unless @key.valid? && @project.deploy_keys << @key
+      flash[:alert] = @key.errors.full_messages.join(', ').html_safe      
     end
+    redirect_to_repository_settings(@project)
   end
 
   def enable
     Projects::EnableDeployKeyService.new(@project, current_user, params).execute
 
-    redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+    redirect_to_repository_settings(@project)
   end
 
   def disable
     @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
 
-    redirect_back_or_default(default: { action: 'index' })
+    redirect_to_repository_settings(@project)
   end
 
   protected
 
-  def set_index_vars
-    @enabled_keys           ||= @project.deploy_keys
-
-    @available_keys         ||= current_user.accessible_deploy_keys - @enabled_keys
-    @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
-    @available_public_keys  ||= DeployKey.are_public - @enabled_keys
-
-    # Public keys that are already used by another accessible project are already
-    # in @available_project_keys.
-    @available_public_keys -= @available_project_keys
-  end
-
   def deploy_key_params
     params.require(:deploy_key).permit(:key, :title, :can_push)
   end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index fed75396d6e072088744718b7170bc8a0ad58f79..fa37963dfd4f77046365687e40777114e39a676a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
   before_action :authorize_create_deployment!, only: [:stop]
   before_action :authorize_update_environment!, only: [:edit, :update]
   before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
-  before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
+  before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
   before_action :verify_api_request!, only: :terminal_websocket_authorize
 
   def index
@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
     end
   end
 
+  def metrics
+    # Currently, this acts as a hint to load the metrics details into the cache
+    # if they aren't there already
+    @metrics = environment.metrics || {}
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: @metrics, status: @metrics.any? ? :ok : :no_content
+      end
+    end
+  end
+
   private
 
   def verify_api_request!
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 216c158e41e0e24b5604ad01f55004899783a822..9a1bf037a95609d39d11e7d25e59a4739510c9a4 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -76,11 +76,12 @@ class Projects::GitHttpClientController < Projects::ApplicationController
     return @project if defined?(@project)
 
     project_id, _ = project_id_with_suffix
-    if project_id.blank?
-      @project = nil
-    else
-      @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
-    end
+    @project =
+      if project_id.blank?
+        nil
+      else
+        Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
+      end
   end
 
   # This method returns two values so that we can parse
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 923e7340e6925163fb845e4542c3a163dfb0bbbc..43fc0c39801b88b48c54e09c5451a2122719d3f5 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -17,6 +17,25 @@ class Projects::GraphsController < Projects::ApplicationController
   end
 
   def commits
+    redirect_to action: 'charts'
+  end
+
+  def languages
+    redirect_to action: 'charts'
+  end
+
+  def charts
+    get_commits
+    get_languages
+  end
+
+  def ci
+    redirect_to charts_namespace_project_pipelines_path(@project.namespace, @project)
+  end
+
+  private
+
+  def get_commits
     @commits = @project.repository.commits(@ref, limit: 2000, skip_merges: true)
     @commits_graph = Gitlab::Graphs::Commits.new(@commits)
     @commits_per_week_days = @commits_graph.commits_per_week_days
@@ -24,15 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController
     @commits_per_month = @commits_graph.commits_per_month
   end
 
-  def ci
-    @charts = {}
-    @charts[:week] = Ci::Charts::WeekChart.new(project)
-    @charts[:month] = Ci::Charts::MonthChart.new(project)
-    @charts[:year] = Ci::Charts::YearChart.new(project)
-    @charts[:build_times] = Ci::Charts::BuildTime.new(project)
-  end
-
-  def languages
+  def get_languages
     @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages
     total = @languages.map(&:last).sum
 
@@ -52,8 +63,6 @@ class Projects::GraphsController < Projects::ApplicationController
     end
   end
 
-  private
-
   def fetch_graph
     @commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true)
     @log = []
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6ef36771ac12d6d89d24a34b9a33fa36b2040b7c..cdb5b4173d3a88b5aadec321c7f1053058b95d88 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
   include IssuableCollections
   include SpammableActions
 
+  prepend_before_action :authenticate_user!, only: [:new]
+
   before_action :redirect_to_external_issue_tracker, only: [:index, :new]
   before_action :module_enabled
   before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -26,7 +28,7 @@ class Projects::IssuesController < Projects::ApplicationController
     @collection_type    = "Issue"
     @issues             = issues_collection
     @issues             = @issues.page(params[:page])
-    @issuable_meta_data = issuable_meta_data(@issues)
+    @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
 
     if @issues.out_of_range? && @issues.total_pages != 0
       return redirect_to url_for(params.merge(page: @issues.total_pages))
@@ -64,8 +66,15 @@ class Projects::IssuesController < Projects::ApplicationController
     params[:issue] ||= ActionController::Parameters.new(
       assignee_id: ""
     )
-    build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
-    @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
+    build_params = issue_params.merge(
+      merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+      discussion_to_resolve: params[:discussion_to_resolve]
+    )
+    service = Issues::BuildService.new(project, current_user, build_params)
+
+    @issue = @noteable = service.execute
+    @merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
+    @discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
 
     respond_with(@issue)
   end
@@ -94,11 +103,21 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def create
-    create_params = issue_params
-      .merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
-      .merge(spammable_params)
+    create_params = issue_params.merge(spammable_params).merge(
+      merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+      discussion_to_resolve: params[:discussion_to_resolve]
+    )
 
-    @issue = Issues::CreateService.new(project, current_user, create_params).execute
+    service = Issues::CreateService.new(project, current_user, create_params)
+    @issue = service.execute
+
+    if service.discussions_to_resolve.count(&:resolved?) > 0
+      flash[:notice] = if service.discussion_to_resolve_id
+                         "Resolved 1 discussion."
+                       else
+                         "Resolved all discussions."
+                       end
+    end
 
     respond_to do |format|
       format.html do
@@ -129,13 +148,12 @@ class Projects::IssuesController < Projects::ApplicationController
       end
 
       format.json do
-        render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+        render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
       end
     end
 
   rescue ActiveRecord::StaleObjectError
-    @conflict = true
-    render :edit
+    render_conflict_response
   end
 
   def referenced_merge_requests
@@ -186,14 +204,6 @@ class Projects::IssuesController < Projects::ApplicationController
   alias_method :awardable, :issue
   alias_method :spammable, :issue
 
-  def merge_request_for_resolving_discussions
-    return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
-
-    @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
-                                                   execute.
-                                                   find_by(iid: merge_request_iid)
-  end
-
   def authorize_read_issue!
     return render_404 unless can?(current_user, :read_issue, @issue)
   end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 75971faa93e6475609b49fb1854a1fce1bda24e1..677a8a1a73a3f687d411d380ad8f91a87469d44a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,11 +10,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   before_action :module_enabled
   before_action :merge_request, only: [
     :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
-    :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
+    :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
   ]
   before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
   before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
-  before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
+  before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check]
   before_action :define_commit_vars, only: [:diffs]
   before_action :define_diff_comment_vars, only: [:diffs]
   before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
@@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @collection_type    = "MergeRequest"
     @merge_requests     = merge_requests_collection
     @merge_requests     = @merge_requests.page(params[:page])
-    @issuable_meta_data = issuable_meta_data(@merge_requests)
+    @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
 
     if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
       return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
@@ -245,9 +245,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       format.json do
         define_pipelines_vars
 
-        render json: PipelineSerializer
+        render json: {
+          pipelines: PipelineSerializer
           .new(project: @project, user: @current_user)
           .represent(@pipelines)
+        }
       end
     end
   end
@@ -296,22 +298,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   def update
     @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
 
-    if @merge_request.valid?
-      respond_to do |format|
-        format.html do
-          redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
-                       @merge_request.target_project, @merge_request])
-        end
-        format.json do
-          render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+    respond_to do |format|
+      format.html do
+        if @merge_request.valid?
+          redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
+        else
+          render :edit
         end
       end
-    else
-      render "edit"
+
+      format.json do
+        render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+      end
     end
   rescue ActiveRecord::StaleObjectError
-    @conflict = true
-    render :edit
+    render_conflict_response
   end
 
   def remove_wip
@@ -323,12 +324,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   def merge_check
     @merge_request.check_if_can_be_merged
+    @pipelines = @merge_request.all_pipelines
 
     render partial: "projects/merge_requests/widget/show.html.haml", layout: false
   end
 
-  def cancel_merge_when_build_succeeds
-    unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+  def cancel_merge_when_pipeline_succeeds
+    unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
       return access_denied!
     end
 
@@ -340,9 +342,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   def merge
     return access_denied! unless @merge_request.can_be_merged_by?(current_user)
 
-    # Disable the CI check if merge_when_build_succeeds is enabled since we have
+    # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
     # to wait until CI completes to know
-    unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?)
+    unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
       @status = :failed
       return
     end
@@ -354,7 +356,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
     @merge_request.update(merge_error: nil)
 
-    if params[:merge_when_build_succeeds].present?
+    if params[:merge_when_pipeline_succeeds].present?
       unless @merge_request.head_pipeline
         @status = :failed
         return
@@ -365,7 +367,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
           .new(@project, current_user, merge_params)
           .execute(@merge_request)
 
-        @status = :merge_when_build_succeeds
+        @status = :merge_when_pipeline_succeeds
       elsif @merge_request.head_pipeline.success?
         # This can be triggered when a user clicks the auto merge button while
         # the tests finish at about the same time
@@ -381,14 +383,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   end
 
   def merge_widget_refresh
-    if merge_request.merge_when_build_succeeds
-      @status = :merge_when_build_succeeds
-    else
-      # Only MRs that can be merged end in this action
-      # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
-      # in last case it does not have any special status. Possible error is handled inside widget js function
-      @status = :success
-    end
+    @status =
+      if merge_request.merge_when_pipeline_succeeds
+        :merge_when_pipeline_succeeds
+      else
+        # Only MRs that can be merged end in this action
+        # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
+        # in last case it does not have any special status. Possible error is handled inside widget js function
+        :success
+      end
 
     render 'merge'
   end
@@ -444,6 +447,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   def ci_status
     pipeline = @merge_request.head_pipeline
+    @pipelines = @merge_request.all_pipelines
 
     if pipeline
       status = pipeline.status
@@ -462,7 +466,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
       status: status,
       coverage: coverage,
-      pipeline: pipeline.try(:id)
+      pipeline: pipeline.try(:id),
+      has_ci: @merge_request.has_ci?
     }
 
     render json: response
@@ -672,8 +677,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @merge_request.ensure_ref_fetched
   end
 
-  def merge_when_build_succeeds_active?
-    params[:merge_when_build_succeeds].present? &&
+  def merge_when_pipeline_succeeds_active?
+    params[:merge_when_pipeline_succeeds].present? &&
       @merge_request.head_pipeline && @merge_request.head_pipeline.active?
   end
 
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index b033f7b5ea94908f05889a6fdec9fb5ee95d8c9d..d00177e7612fb3b1a371ada907396ce4b79876b9 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -148,17 +148,10 @@ class Projects::NotesController < Projects::ApplicationController
 
   def note_json(note)
     attrs = {
-      award: false,
       id: note.id
     }
 
-    if note.is_a?(AwardEmoji)
-      attrs.merge!(
-        valid:  note.valid?,
-        award:  true,
-        name:   note.name
-      )
-    elsif note.persisted?
+    if note.persisted?
       Banzai::NoteRenderer.render([note], @project, current_user)
 
       attrs.merge!(
@@ -198,7 +191,7 @@ class Projects::NotesController < Projects::ApplicationController
       )
     end
 
-    attrs[:commands_changes] = note.commands_changes unless attrs[:award]
+    attrs[:commands_changes] = note.commands_changes
     attrs
   end
 
@@ -218,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController
   end
 
   def find_current_user_notes
-    @notes = NotesFinder.new(project, current_user, params).execute.inc_author
+    @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
+      .execute.inc_author
+  end
+
+  def last_fetched_at
+    request.headers['X-Last-Fetched-At']
   end
 end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 84451257b98f898946e27f68af46b40ad3f9df9f..718d9e86beaad2db77c8dd894f5ca39ac6f0f2e4 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,9 +1,10 @@
 class Projects::PipelinesController < Projects::ApplicationController
-  before_action :pipeline, except: [:index, :new, :create]
+  before_action :pipeline, except: [:index, :new, :create, :charts]
   before_action :commit, only: [:show, :builds]
   before_action :authorize_read_pipeline!
   before_action :authorize_create_pipeline!, only: [:new, :create]
   before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+  before_action :builds_enabled, only: :charts
 
   def index
     @scope = params[:scope]
@@ -13,9 +14,15 @@ class Projects::PipelinesController < Projects::ApplicationController
       .page(params[:page])
       .per(30)
 
-    @running_or_pending_count = PipelinesFinder
+    @running_count = PipelinesFinder
       .new(project).execute(scope: 'running').count
 
+    @pending_count = PipelinesFinder
+      .new(project).execute(scope: 'pending').count
+
+    @finished_count = PipelinesFinder
+      .new(project).execute(scope: 'finished').count
+
     @pipelines_count = PipelinesFinder
       .new(project).execute.count
 
@@ -29,7 +36,9 @@ class Projects::PipelinesController < Projects::ApplicationController
             .represent(@pipelines),
           count: {
             all: @pipelines_count,
-            running_or_pending: @running_or_pending_count
+            running: @running_count,
+            pending: @pending_count,
+            finished: @finished_count,
           }
         }
       end
@@ -84,6 +93,14 @@ class Projects::PipelinesController < Projects::ApplicationController
     redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
   end
 
+  def charts
+    @charts = {}
+    @charts[:week] = Ci::Charts::WeekChart.new(project)
+    @charts[:month] = Ci::Charts::MonthChart.new(project)
+    @charts[:year] = Ci::Charts::YearChart.new(project)
+    @charts[:build_times] = Ci::Charts::BuildTime.new(project)
+  end
+
   private
 
   def create_params
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 2f422d352edd6fdde0629c987aa1655084db6218..a8cb07eb67a2c8656563b3693024a2f2bd7ff4e3 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,26 +1,22 @@
 class Projects::ProtectedBranchesController < Projects::ApplicationController
+  include RepositorySettingsRedirect
   # Authorize
   before_action :require_non_empty_project
   before_action :authorize_admin_project!
   before_action :load_protected_branch, only: [:show, :update, :destroy]
-  before_action :load_protected_branches, only: [:index]
 
   layout "project_settings"
 
   def index
-    @protected_branch = @project.protected_branches.new
-    load_gon_index
+    redirect_to_repository_settings(@project)
   end
 
   def create
     @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
-    if @protected_branch.persisted?
-      redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
-    else
-      load_protected_branches
-      load_gon_index
-      render :index
+    unless @protected_branch.persisted?
+      flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
     end
+    redirect_to_repository_settings(@project)
   end
 
   def show
@@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
     @protected_branch.destroy
 
     respond_to do |format|
-      format.html { redirect_to namespace_project_protected_branches_path }
+      format.html { redirect_to_repository_settings(@project) }
       format.js { head :ok }
     end
   end
@@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
                                              merge_access_levels_attributes: [:access_level, :id],
                                              push_access_levels_attributes: [:access_level, :id])
   end
-
-  def load_protected_branches
-    @protected_branches = @project.protected_branches.order(:name).page(params[:page])
-  end
-
-  def access_levels_options
-    {
-      push_access_levels: {
-        "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
-      },
-      merge_access_levels: {
-        "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
-      }
-    }
-  end
-
-  def load_gon_index
-    params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
-    gon.push(params.merge(access_levels_options))
-  end
 end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 10d24da16d7b5052b4572fab0cb6c9b180110421..c55b37ae0dd6359402d5420186935678309e8420 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
 
       return if cached_blob?
 
-      if @blob.lfs_pointer?
+      if @blob.lfs_pointer? && project.lfs_enabled?
         send_lfs_object
       else
         send_git_blob @repository, @blob
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 17cb1d5be242b4b4dfe7fcaa1fd5f80db7d107a9..f9d798d045559c7641ff00652448ebcf2395d258 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
   end
 
   def update
-    if @service.update_attributes(service_params[:service])
+    @service.assign_attributes(service_params[:service])
+    if @service.save(context: :manual_change)
       redirect_to(
         edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
         notice: 'Successfully updated.'
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
index 5735e281f66e772b5b31600ef71e777a233d94d6..cbfa2afa959735470916d4e47f64c80dae584529 100644
--- a/app/controllers/projects/settings/members_controller.rb
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -7,47 +7,18 @@ module Projects
         @sort = params[:sort].presence || sort_value_name
         @group_links = @project.project_group_links
 
-        @project_members = @project.project_members
-        @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
-        group = @project.group
-
-        # group links
-        @group_links = @project.project_group_links.all
-
         @skip_groups = @group_links.pluck(:group_id)
         @skip_groups << @project.namespace_id unless @project.personal?
 
-        if group
-          # We need `.where.not(user_id: nil)` here otherwise when a group has an
-          # invitee, it would make the following query return 0 rows since a NULL
-          # user_id would be present in the subquery
-          # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
-          group_members = MembersFinder.new(@project_members, group).execute(current_user)
-        end
+        @project_members = MembersFinder.new(@project, current_user).execute
 
         if params[:search].present?
-          user_ids = @project.users.search(params[:search]).select(:id)
-          @project_members = @project_members.where(user_id: user_ids)
-
-          if group_members
-            user_ids = group.users.search(params[:search]).select(:id)
-            group_members = group_members.where(user_id: user_ids)
-          end
-
-          @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+          @project_members = @project_members.joins(:user).merge(User.search(params[:search]))
+          @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
         end
 
-        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 ')).
-          sort(@sort).
-          page(params[:page])
-
+        @project_members = @project_members.sort(@sort).page(params[:page])
         @requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
         @project_member = @project.project_members.new
       end
     end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6ce4abca45efc289a54f3b47d8743b57c022a9c
--- /dev/null
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -0,0 +1,50 @@
+module Projects
+  module Settings
+    class RepositoryController < Projects::ApplicationController
+      before_action :authorize_admin_project!
+
+      def show
+        @deploy_keys = DeployKeysPresenter
+          .new(@project, current_user: current_user)
+
+        define_protected_branches
+      end
+
+      private
+
+      def define_protected_branches
+        load_protected_branches
+        @protected_branch = @project.protected_branches.new
+        load_gon_index
+      end
+
+      def load_protected_branches
+        @protected_branches = @project.protected_branches.order(:name).page(params[:page])
+      end
+
+      def access_levels_options
+        {
+          push_access_levels: {
+            roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
+              { id: id, text: text, before_divider: true }
+            end
+          },
+          merge_access_levels: {
+            roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
+              { id: id, text: text, before_divider: true }
+            end
+          }
+        }
+      end
+
+      def open_branches
+        branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
+        { open_branches: branches }
+      end
+
+      def load_gon_index
+        gon.push(open_branches.merge(access_levels_options))
+      end
+    end
+  end
+end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 33379659d739280ecd48de145b11d12a05f452be..e13f0bde315bef97d0fac386006da9fa27baf95d 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController
     @tags = TagsFinder.new(@repository, params).execute
     @tags = Kaminari.paginate_array(@tags).page(params[:page])
 
-    @releases = project.releases.where(tag: @tags.map(&:name))
+    tag_names = @tags.map(&:name)
+    @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
+    @releases = project.releases.where(tag: tag_names)
   end
 
   def show
@@ -41,13 +43,27 @@ class Projects::TagsController < Projects::ApplicationController
   end
 
   def destroy
-    Tags::DestroyService.new(project, current_user).execute(params[:id])
+    result = Tags::DestroyService.new(project, current_user).execute(params[:id])
 
     respond_to do |format|
-      format.html do
-        redirect_to namespace_project_tags_path(@project.namespace, @project)
+      if result[:status] == :success
+        format.html do
+          redirect_to namespace_project_tags_path(@project.namespace, @project)
+        end
+
+        format.js
+      else
+        @error = result[:message]
+
+        format.html do
+          redirect_to namespace_project_tags_path(@project.namespace, @project),
+            alert: @error
+        end
+
+        format.js do
+          render status: :unprocessable_entity
+        end
       end
-      format.js
     end
   end
 end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index b2c11ea4156d280cff1ba443c465b6609f16815d..c47198c5eb678ecc8f7ce754acfc25dc97d28f37 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,5 +1,8 @@
 class Projects::TriggersController < Projects::ApplicationController
   before_action :authorize_admin_build!
+  before_action :authorize_manage_trigger!, except: [:index, :create]
+  before_action :authorize_admin_trigger!, only: [:edit, :update]
+  before_action :trigger, only: [:take_ownership, :edit, :update, :destroy]
 
   layout 'project_settings'
 
@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController
   end
 
   def create
-    @trigger = project.triggers.new
-    @trigger.save
+    @trigger = project.triggers.create(create_params.merge(owner: current_user))
 
     if @trigger.valid?
-      redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
+      flash[:notice] = 'Trigger was created successfully.'
     else
-      @triggers = project.triggers.select(&:persisted?)
-      render action: "show"
+      flash[:alert] = 'You could not create a new trigger.'
+    end
+
+    redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
+  end
+
+  def take_ownership
+    if trigger.update(owner: current_user)
+      flash[:notice] = 'Trigger was re-assigned.'
+    else
+      flash[:alert] = 'You could not take ownership of trigger.'
+    end
+
+    redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
+  end
+
+  def edit
+  end
+
+  def update
+    if trigger.update(update_params)
+      redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
+    else
+      render action: "edit"
     end
   end
 
   def destroy
-    trigger.destroy
-    flash[:alert] = "Trigger removed"
+    if trigger.destroy
+      flash[:notice] = "Trigger removed."
+    else
+      flash[:alert] = "Could not remove the trigger."
+    end
 
     redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
   end
 
   private
 
+  def authorize_manage_trigger!
+    access_denied! unless can?(current_user, :manage_trigger, trigger)
+  end
+
+  def authorize_admin_trigger!
+    access_denied! unless can?(current_user, :admin_trigger, trigger)
+  end
+
   def trigger
-    @trigger ||= project.triggers.find(params[:id])
+    @trigger ||= project.triggers.find(params[:id]) || render_404
+  end
+
+  def create_params
+    params.require(:trigger).permit(:description)
+  end
+
+  def update_params
+    params.require(:trigger).permit(:description)
   end
 end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 2d8064c9878f871c6e035b01b768960ec984768e..f210f7e61d2699ae7f04531c262a7fd55e4fe8b5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,5 +1,3 @@
-require 'project_wiki'
-
 class Projects::WikisController < Projects::ApplicationController
   before_action :authorize_read_wiki!
   before_action :authorize_create_wiki!, only: [:edit, :create, :history]
@@ -47,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
     return render('empty') unless can?(current_user, :create_wiki, @project)
 
     @page = @project_wiki.find_page(params[:id])
+    @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
 
-    if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+    if @page.valid?
       redirect_to(
         namespace_project_wiki_path(@project.namespace, @project, @page),
         notice: 'Wiki was successfully updated.'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index acca821782c7d17205fbc7f957158972a668aebb..47f7e0b1b280fcacc124b2037ed8cea545cd59ae 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -117,7 +117,7 @@ class ProjectsController < Projects::ApplicationController
     return access_denied! unless can?(current_user, :remove_project, @project)
 
     ::Projects::DestroyService.new(@project, current_user, {}).async_execute
-    flash[:alert] = "Project '#{@project.name}' will be deleted."
+    flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted."
 
     redirect_to dashboard_projects_path
   rescue Projects::DestroyService::DestroyError => ex
@@ -267,8 +267,9 @@ class ProjectsController < Projects::ApplicationController
         @project_wiki = @project.wiki
         @wiki_home = @project_wiki.find_page('home', params[:version_id])
       elsif @project.feature_available?(:issues, current_user)
-        @issues = issues_collection
-        @issues = @issues.page(params[:page])
+        @issues = issues_collection.page(params[:page])
+        @collection_type = 'Issue'
+        @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
       end
 
       render :show
@@ -314,7 +315,8 @@ class ProjectsController < Projects::ApplicationController
       :name,
       :namespace_id,
       :only_allow_merge_if_all_discussions_are_resolved,
-      :only_allow_merge_if_build_succeeds,
+      :only_allow_merge_if_pipeline_succeeds,
+      :printing_merge_request_link_enabled,
       :path,
       :public_builds,
       :request_access_enabled,
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index db2817fadf6d9271cfa6b604d7ce67b945a13bdf..1b4545e4a49268e1ea7b0091ee27257663969848 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -8,7 +8,9 @@
 # `DashboardController#show`, which is the default.
 class RootController < Dashboard::ProjectsController
   skip_before_action :authenticate_user!, only: [:index]
-  before_action :redirect_to_custom_dashboard, only: [:index]
+
+  before_action :redirect_unlogged_user, if: -> { current_user.nil? }
+  before_action :redirect_logged_user, if: -> { current_user.present? }
 
   def index
     super
@@ -16,23 +18,38 @@ class RootController < Dashboard::ProjectsController
 
   private
 
-  def redirect_to_custom_dashboard
-    return redirect_to new_user_session_path unless current_user
+  def redirect_unlogged_user
+    if redirect_to_home_page_url?
+      redirect_to(current_application_settings.home_page_url)
+    else
+      redirect_to(new_user_session_path)
+    end
+  end
 
+  def redirect_logged_user
     case current_user.dashboard
     when 'stars'
       flash.keep
-      redirect_to starred_dashboard_projects_path
+      redirect_to(starred_dashboard_projects_path)
     when 'project_activity'
-      redirect_to activity_dashboard_path
+      redirect_to(activity_dashboard_path)
     when 'starred_project_activity'
-      redirect_to activity_dashboard_path(filter: 'starred')
+      redirect_to(activity_dashboard_path(filter: 'starred'))
     when 'groups'
-      redirect_to dashboard_groups_path
+      redirect_to(dashboard_groups_path)
     when 'todos'
-      redirect_to dashboard_todos_path
-    else
-      return
+      redirect_to(dashboard_todos_path)
     end
   end
+
+  def redirect_to_home_page_url?
+    # If user is not signed-in and tries to access root_path - redirect him to landing page
+    # Don't redirect to the default URL to prevent endless redirections
+    return false unless current_application_settings.home_page_url.present?
+
+    home_page_url = current_application_settings.home_page_url.chomp('/')
+    root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
+
+    root_urls.exclude?(home_page_url)
+  end
 end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 93a180b903609f2e42246f53ce30d8bf56d98e7b..7d81c96262f89a29d4d768e47c2e00b73615ed3a 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -15,11 +15,12 @@ class SessionsController < Devise::SessionsController
 
   def new
     set_minimum_password_length
-    if Gitlab.config.ldap.enabled
-      @ldap_servers = Gitlab::LDAP::Config.servers
-    else
-      @ldap_servers = []
-    end
+    @ldap_servers =
+      if Gitlab.config.ldap.enabled
+        Gitlab::LDAP::Config.servers
+      else
+        []
+      end
 
     super
   end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2d26718873f98ff8a16da968b3562d40a263fef0..f3fd3da8b2033b5629f380a644190e45b1ea55b3 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -28,8 +28,9 @@ class SnippetsController < ApplicationController
       @snippets = SnippetsFinder.new.execute(current_user, {
         filter: :by_user,
         user: @user,
-        scope: params[:scope] }).
-      page(params[:page])
+        scope: params[:scope]
+      })
+      .page(params[:page])
 
       render 'index'
     else
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 509f4f412ca3cf738684e886149e52fbe3a66b92..f1bfd574f04dfd8bb10181f0f2ab592a3fc22990 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
     end
 
     disposition = uploader.image? ? 'inline' : 'attachment'
+
+    expires_in 0.seconds, must_revalidate: true, private: true
     send_file uploader.file.path, disposition: disposition
   end
 
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 9f2206346ce065711df474c073b2746eb4b9e07d..fce3775f40e57fc51f1124bff256e0f3128738c8 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -1,4 +1,4 @@
-class GroupMembersFinder < Projects::ApplicationController
+class GroupMembersFinder
   def initialize(group)
     @group = group
   end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 206c92fe82af566b1ba93749b0febcd125124f9b..f7ebb1807d7ae0931f13729cc39020d8cdbcba2b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,7 +19,7 @@
 #     iids: integer[]
 #
 class IssuableFinder
-  NONE = '0'
+  NONE = '0'.freeze
 
   attr_accessor :current_user, :params
 
@@ -33,15 +33,17 @@ class IssuableFinder
     items = by_scope(items)
     items = by_state(items)
     items = by_group(items)
-    items = by_project(items)
     items = by_search(items)
-    items = by_milestone(items)
     items = by_assignee(items)
     items = by_author(items)
-    items = by_label(items)
     items = by_due_date(items)
     items = by_non_archived(items)
     items = by_iids(items)
+    items = by_milestone(items)
+    items = by_label(items)
+
+    # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
+    items = by_project(items)
     sort(items)
   end
 
@@ -107,8 +109,7 @@ class IssuableFinder
     @project = project
   end
 
-  def projects
-    return @projects if defined?(@projects)
+  def projects(items = nil)
     return @projects = project if project?
 
     projects =
@@ -117,7 +118,7 @@ class IssuableFinder
       elsif group
         GroupProjectsFinder.new(group).execute(current_user)
       else
-        ProjectsFinder.new.execute(current_user)
+        projects_finder.execute(current_user, item_project_ids(items))
       end
 
     @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -257,9 +258,9 @@ class IssuableFinder
   def by_project(items)
     items =
       if project?
-        items.of_projects(projects).references_project
-      elsif projects
-        items.merge(projects.reorder(nil)).join_project
+        items.of_projects(projects(items)).references_project
+      elsif projects(items)
+        items.merge(projects(items).reorder(nil)).join_project
       else
         items.none
       end
@@ -309,18 +310,25 @@ class IssuableFinder
     params[:milestone_title] == Milestone::Upcoming.name
   end
 
+  def filter_by_started_milestone?
+    params[:milestone_title] == Milestone::Started.name
+  end
+
   def by_milestone(items)
     if milestones?
       if filter_by_no_milestone?
         items = items.left_joins_milestones.where(milestone_id: [-1, nil])
       elsif filter_by_upcoming_milestone?
-        upcoming_ids = Milestone.upcoming_ids_by_projects(projects)
+        upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
         items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
+      elsif filter_by_started_milestone?
+        items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
       else
         items = items.with_milestone(params[:milestone_title])
+        items_projects = projects(items)
 
-        if projects
-          items = items.where(milestones: { project_id: projects })
+        if items_projects
+          items = items.where(milestones: { project_id: items_projects })
         end
       end
     end
@@ -334,9 +342,10 @@ class IssuableFinder
         items = items.without_label
       else
         items = items.with_label(label_names, params[:sort])
+        items_projects = projects(items)
 
-        if projects
-          label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
+        if items_projects
+          label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
           items = items.where(labels: { id: label_ids })
         end
       end
@@ -396,4 +405,8 @@ class IssuableFinder
   def current_user_related?
     params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
   end
+
+  def projects_finder
+    @projects_finder ||= ProjectsFinder.new
+  end
 end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index f542f72a386c6d5ad3ffbe094ae623e1f608d17d..087132729472ba214148b5f52b8a915f5cfb777a 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder
       user_id: user.id,
       project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
   end
+
+  def item_project_ids(items)
+    items&.reorder(nil)&.select(:project_id)
+  end
 end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 702944404f50909d3fcb37f1ea5eb2b440f9c1fd..af24045886ee4850d99de9d72799d28544950ada 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -1,13 +1,35 @@
-class MembersFinder < Projects::ApplicationController
-  def initialize(project_members, project_group)
-    @project_members = project_members
-    @project_group = project_group
+class MembersFinder
+  attr_reader :project, :current_user, :group
+
+  def initialize(project, current_user)
+    @project = project
+    @current_user = current_user
+    @group = project.group
+  end
+
+  def execute
+    project_members = project.project_members
+    project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
+    wheres = ["members.id IN (#{project_members.select(:id).to_sql})"]
+
+    if group
+      # We need `.where.not(user_id: nil)` here otherwise when a group has an
+      # invitee, it would make the following query return 0 rows since a NULL
+      # user_id would be present in the subquery
+      # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+      non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id)
+
+      group_members = GroupMembersFinder.new(group).execute
+      group_members = group_members.where.not(user_id: non_null_user_ids)
+      group_members = group_members.non_invite unless can?(current_user, :admin_group, group)
+
+      wheres << "members.id IN (#{group_members.select(:id).to_sql})"
+    end
+
+    Member.where(wheres.join(' OR '))
   end
 
-  def execute(current_user)
-    non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
-    group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
-    group_members = group_members.non_invite unless can?(current_user, :admin_group,  @project_group)
-    group_members
+  def can?(*args)
+    Ability.allowed?(*args)
   end
 end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b76ca389f382ccebe4551ddccaa5376d74bd91e8..1eec45d9cb512cef7ad09730257aad9f905f76c3 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder
   def klass
     MergeRequest
   end
+
+  private
+
+  def item_project_ids(items)
+    items&.reorder(nil)&.select(:target_project_id)
+  end
 end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 4bd8c83081a479238298b8554dca3a876710a93c..6630c6384f23885a2671735fc89cbf84626c5bef 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -28,11 +28,12 @@ class NotesFinder
   private
 
   def init_collection
-    if @params[:target_id]
-      @notes = on_target(@params[:target_type], @params[:target_id])
-    else
-      @notes = notes_of_any_type
-    end
+    @notes =
+      if @params[:target_id]
+        on_target(@params[:target_type], @params[:target_id])
+      else
+        notes_of_any_type
+      end
   end
 
   def notes_of_any_type
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..760166b453ffadb6bc03a0116238be02b1eafa20
--- /dev/null
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -0,0 +1,45 @@
+class PersonalAccessTokensFinder
+  attr_accessor :params
+
+  delegate :build, :find, :find_by, to: :execute
+
+  def initialize(params = {})
+    @params = params
+  end
+
+  def execute
+    tokens = PersonalAccessToken.all
+    tokens = by_user(tokens)
+    tokens = by_impersonation(tokens)
+    by_state(tokens)
+  end
+
+  private
+
+  def by_user(tokens)
+    return tokens unless @params[:user]
+    tokens.where(user: @params[:user])
+  end
+
+  def by_impersonation(tokens)
+    case @params[:impersonation]
+    when true
+      tokens.with_impersonation
+    when false
+      tokens.without_impersonation
+    else
+      tokens
+    end
+  end
+
+  def by_state(tokens)
+    case @params[:state]
+    when 'active'
+      tokens.active
+    when 'inactive'
+      tokens.inactive
+    else
+      tokens
+    end
+  end
+end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 32aea75486deee72a465d8f5959a772d1787df79..a9172f6767fb42ca51339d5b1af49d61559857f7 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -10,7 +10,11 @@ class PipelinesFinder
     scoped_pipelines =
       case scope
       when 'running'
-        pipelines.running_or_pending
+        pipelines.running
+      when 'pending'
+        pipelines.pending
+      when 'finished'
+        pipelines.finished
       when 'branches'
         from_ids(ids_for_ref(branches))
       when 'tags'
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index a93a63bdb9b7892650063caf1763ae1360bfe817..13d33a1c31b19b3b3209b24aa66ef8dac85b398e 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -13,7 +13,7 @@
 #
 
 class TodosFinder
-  NONE = '0'
+  NONE = '0'.freeze
 
   attr_accessor :current_user, :params
 
@@ -24,6 +24,7 @@ class TodosFinder
 
   def execute
     items = current_user.todos
+    items = include_associations(items)
     items = by_action_id(items)
     items = by_action(items)
     items = by_author(items)
@@ -38,6 +39,17 @@ class TodosFinder
 
   private
 
+  def include_associations(items)
+    return items unless params[:include_associations]
+
+    items.includes(
+      [
+        target: { project: [:route, namespace: :route] },
+        author: { namespace: :route },
+      ]
+    )
+  end
+
   def action_id?
     action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
   end
@@ -99,7 +111,7 @@ class TodosFinder
   end
 
   def type?
-    type.present? && ['Issue', 'MergeRequest'].include?(type)
+    type.present? && %w(Issue MergeRequest).include?(type)
   end
 
   def type
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6db813d4a02783c4ed0aa9e4de6300dc1ff3191b..a3213581498abba6f8c8de3af5671343054f75e5 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -69,11 +69,12 @@ module ApplicationHelper
   end
 
   def avatar_icon(user_or_email = nil, size = nil, scale = 2)
-    if user_or_email.is_a?(User)
-      user = user_or_email
-    else
-      user = User.find_by_any_email(user_or_email.try(:downcase))
-    end
+    user =
+      if user_or_email.is_a?(User)
+        user_or_email
+      else
+        User.find_by_any_email(user_or_email.try(:downcase))
+      end
 
     if user
       user.avatar_url(size) || default_avatar
@@ -166,7 +167,7 @@ module ApplicationHelper
     css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
     css_classes << " #{html_class}" unless html_class.blank?
 
-    element = content_tag :time, time.to_s,
+    element = content_tag :time, time.strftime("%b %d, %Y"),
       class: css_classes,
       title: time.to_time.in_time_zone.to_s(:medium),
       datetime: time.to_time.getutc.iso8601,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 604851604955fd5247052a3fd0b02cda11a549e1..ca326dd0627f022d55a98a41597041ad4b9901ea 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,28 +1,15 @@
 module ApplicationSettingsHelper
-  def gravatar_enabled?
-    current_application_settings.gravatar_enabled?
-  end
-
-  def signup_enabled?
-    current_application_settings.signup_enabled?
-  end
-
-  def signin_enabled?
-    current_application_settings.signin_enabled?
-  end
+  delegate  :gravatar_enabled?,
+            :signup_enabled?,
+            :signin_enabled?,
+            :akismet_enabled?,
+            :koding_enabled?,
+            to: :current_application_settings
 
   def user_oauth_applications?
     current_application_settings.user_oauth_applications
   end
 
-  def askimet_enabled?
-    current_application_settings.akismet_enabled?
-  end
-
-  def koding_enabled?
-    current_application_settings.koding_enabled?
-  end
-
   def allowed_protocols_present?
     current_application_settings.enabled_git_access_protocol.present?
   end
@@ -94,8 +81,8 @@ module ApplicationSettingsHelper
   end
 
   def repository_storages_options_for_select
-    options = Gitlab.config.repositories.storages.map do |name, path|
-      ["#{name} - #{path}", name]
+    options = Gitlab.config.repositories.storages.map do |name, storage|
+      ["#{name} - #{storage['path']}", name]
     end
 
     options_for_select(options, @application_setting.repository_storages)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 311a70725ab07ab67cc663619a45f0fd76000ebb..8631bc54509b3347ccd6d777577797684c58edf6 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -153,16 +153,17 @@ module BlobHelper
     # Because we are opionated we set the cache headers ourselves.
     response.cache_control[:public] = @project.public?
 
-    if @ref && @commit && @ref == @commit.id
-      # This is a link to a commit by its commit SHA. That means that the blob
-      # is immutable. The only reason to invalidate the cache is if the commit
-      # was deleted or if the user lost access to the repository.
-      response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
-    else
-      # A branch or tag points at this blob. That means that the expected blob
-      # value may change over time.
-      response.cache_control[:max_age] = Blob::CACHE_TIME
-    end
+    response.cache_control[:max_age] =
+      if @ref && @commit && @ref == @commit.id
+        # This is a link to a commit by its commit SHA. That means that the blob
+        # is immutable. The only reason to invalidate the cache is if the commit
+        # was deleted or if the user lost access to the repository.
+        Blob::CACHE_TIME_IMMUTABLE
+      else
+        # A branch or tag points at this blob. That means that the expected blob
+        # value may change over time.
+        Blob::CACHE_TIME
+      end
 
     response.etag = @blob.id
     !stale
@@ -202,4 +203,18 @@ module BlobHelper
       'blob-language' => @blob && @blob.language.try(:ace_mode)
     }
   end
+
+  def copy_file_path_button(file_path)
+    clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+  end
+
+  def copy_blob_content_button(blob)
+    return if markup?(blob.name)
+
+    clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
+  end
+
+  def open_raw_file_button(path)
+    link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
+  end
 end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index ff937b5ebd201805b5c9263bcfd90533ba6a3648..2fcb7a59fc32a04d30af6b0663c71126e712947f 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -12,7 +12,14 @@ module BuildsHelper
       build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
       build_status: @build.status,
       build_stage: @build.stage,
-      log_state: @build.trace_with_state[:state].to_s
+      log_state: ''
+    }
+  end
+
+  def build_failed_issue_options
+    {
+      title: "Build Failed ##{@build.id}",
+      description: namespace_project_build_url(@project.namespace, @project, @build)
     }
   end
 end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 4c7c16d694c624c866b2e0ac009620f11b2c2ef2..0b30471f2ae065b88bf9c24a3a72a7514e5ef44b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -19,7 +19,7 @@ module ButtonHelper
     title = data[:title] || 'Copy to clipboard'
     data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
     content_tag :button,
-      icon('clipboard'),
+      icon('clipboard', 'aria-hidden': 'true'),
       class: "btn #{css_class}",
       data: data,
       type: :button,
@@ -34,7 +34,7 @@ module ButtonHelper
 
     content_tag (append_link ? :a : :span), protocol,
       class: klass,
-      href: (project.http_url_to_repo if append_link),
+      href: (project.http_url_to_repo(current_user) if append_link),
       data: {
         html: true,
         placement: placement,
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 94f3b48017814cf8d74ab363ed585d3d4ef7187e..2de9e0de310730cd1d670100904770ad6ad84726 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -15,6 +15,8 @@ module CiStatusHelper
       'passed'
     when 'success_with_warnings'
       'passed with warnings'
+    when 'manual'
+      'waiting for manual action'
     else
       status
     end
@@ -48,6 +50,8 @@ module CiStatusHelper
         'icon_status_created'
       when 'skipped'
         'icon_status_skipped'
+      when 'manual'
+        'icon_status_manual'
       else
         'icon_status_canceled'
       end
@@ -55,6 +59,24 @@ module CiStatusHelper
     custom_icon(icon_name)
   end
 
+  def pipeline_status_cache_key(pipeline_status)
+    "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
+  end
+
+  def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
+    project = pipeline_status.project
+    path = pipelines_namespace_project_commit_path(
+      project.namespace,
+      project,
+      pipeline_status.sha)
+
+    render_status_with_link(
+      'commit',
+      pipeline_status.status,
+      path,
+      tooltip_placement: tooltip_placement)
+  end
+
   def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
     project = commit.project
     path = pipelines_namespace_project_commit_path(
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 8aad39e148bd2a7d8764b71fff3f14ea6d866f39..cef624430da0fd7709fec47947a1d8278daa1752 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -211,7 +211,7 @@ module CommitsHelper
     external_url = environment.external_url_for(diff_new_path, commit_sha)
     return unless external_url
 
-    link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+    link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
       icon('external-link')
     end
   end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 2843ad96efadbb2dd63495db80c8e402febb7fa6..f927cfc998f0e6c952f20942160344b68ae1f2ce 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,4 +1,6 @@
 module EmailsHelper
+  include AppearancesHelper
+
   # Google Actions
   # https://developers.google.com/gmail/markup/reference/go-to-action
   def email_action(url)
@@ -22,7 +24,7 @@ module EmailsHelper
 
   def action_title(url)
     return unless url
-    ["merge_requests", "issues", "commit"].each do |action|
+    %w(merge_requests issues commit).each do |action|
       if url.split("/").include?(action)
         return "View #{action.humanize.singularize}"
       end
@@ -49,4 +51,19 @@ module EmailsHelper
     msg = "This link is valid for #{password_reset_token_valid_time}.  "
     msg << "After it expires, you can #{link_tag}."
   end
+
+  def header_logo
+    if brand_item && brand_item.header_logo?
+      image_tag(
+        brand_item.header_logo,
+        style: 'height: 50px'
+      )
+    else
+      image_tag(
+        image_url('mailers/gitlab_header_logo.gif'),
+        size: "55x50",
+        alt: "GitLab"
+      )
+    end
+  end
 end
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..482f68f412bff2e769f62f4c6b06183afb12a4f8
--- /dev/null
+++ b/app/helpers/emoji_helper.rb
@@ -0,0 +1,5 @@
+module EmojiHelper
+  def emoji_icon(*args)
+    raw Gitlab::Emoji.gl_emoji_tag(*args)
+  end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 362046c027012a027976ad83985843ed1300a661..fb872a13f740a40c325389b2a8552b2808b25337 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -162,7 +162,12 @@ module EventsHelper
 
   def event_note(text, options = {})
     text = first_line_in_markdown(text, 150, options)
-    sanitize(text, tags: %w(a img b pre code p span))
+
+    sanitize(
+      text,
+      tags: %w(a img gl-emoji b pre code p span),
+      attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version']
+    )
   end
 
   def event_commit_title(message)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 2b1f3825adc1525284df97aef4f1439f28c28d15..7bd212a3ef9474788a08862be82a95a396eee02a 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,20 +1,33 @@
 module ExploreHelper
   def filter_projects_path(options = {})
     exist_opts = {
-      sort: params[:sort],
+      sort: params[:sort] || @sort,
       scope: params[:scope],
       group: params[:group],
       tag: params[:tag],
       visibility_level: params[:visibility_level],
+      name: params[:name],
+      personal: params[:personal],
+      archived: params[:archived],
+      shared: params[:shared],
+      namespace_id: params[:namespace_id],
     }
 
-    options = exist_opts.merge(options)
-    path = request.path
-    path << "?#{options.to_param}"
-    path
+    options = exist_opts.merge(options).delete_if { |key, value| value.blank? }
+    request_path_with_options(options)
+  end
+
+  def filter_groups_path(options = {})
+    request_path_with_options(options)
   end
 
   def explore_controller?
     controller.class.name.split("::").first == "Explore"
   end
+
+  private
+
+  def request_path_with_options(options = {})
+    request.path + "?#{options.to_param}"
+  end
 end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 6d365ea925140029405e5ce009d8c8406c314f13..cd442237086599e871921dc17efccc4498b40130 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
   # text hasn't already been truncated, then append "..." to the node contents
   # and return true.  Otherwise return false.
   def truncate_if_block(node, truncated)
-    if node.element? && node.description.block? && !truncated
+    return true if truncated
+
+    if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
       node.inner_html = "#{node.inner_html}..." if node.next_sibling
       true
     else
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f16a63e21789212635bf77313068dadf570c7c44..e9b7cbbad6a5e328f088f98f9d1c86168f381580 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -74,6 +74,10 @@ module GitlabRoutingHelper
     namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
   end
 
+  def environment_metrics_path(environment, *args)
+    metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+  end
+
   def issue_path(entity, *args)
     namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
   end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 926c9703628158e1027666936133faa1da3469f3..a6014088e929f2dbf76f0d03aa519b5e04deedbc 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,17 +12,18 @@ module GroupsHelper
   end
 
   def group_title(group, name = nil, url = nil)
+    @has_group_title = true
     full_title = ''
 
     group.ancestors.each do |parent|
-      full_title += link_to(simple_sanitize(parent.name), group_path(parent))
-      full_title += ' / '.html_safe
+      full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
+      full_title += '<span class="hidable"> / </span>'.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
+    full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path')
+    full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
 
-    content_tag :span do
+    content_tag :span, class: 'group-title' do
       full_title.html_safe
     end
   end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a0642a1894b6413b8c4ef7d52f416f32729390bb..a57b5a8fea5d4ce7a09774582360843e9cd0919f 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -7,7 +7,7 @@ module ImportHelper
   def provider_project_link(provider, path_with_namespace)
     url = __send__("#{provider}_project_url", path_with_namespace)
 
-    link_to path_with_namespace, url, target: '_blank'
+    link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
   end
 
   private
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03354c235eb0763ce33943846fab103f0c944e6c..a777db2826b6ddfe10795292dc8a72a44464450c 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -1,6 +1,8 @@
 module IssuablesHelper
+  include GitlabRoutingHelper
+
   def sidebar_gutter_toggle_icon
-    sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right')
+    sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' })
   end
 
   def sidebar_gutter_collapsed_class
@@ -23,7 +25,7 @@ module IssuablesHelper
   def issuable_json_path(issuable)
     project = issuable.project
 
-    if issuable.kind_of?(MergeRequest)
+    if issuable.is_a?(MergeRequest)
       namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
     else
       namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
@@ -52,7 +54,7 @@ module IssuablesHelper
         field_name: 'issuable_template',
         selected: selected_template(issuable),
         project_path: ref_project.path,
-        namespace_path: ref_project.namespace.path
+        namespace_path: ref_project.namespace.full_path
       }
     }
 
@@ -88,15 +90,33 @@ module IssuablesHelper
   end
 
   def milestone_dropdown_label(milestone_title, default_label = "Milestone")
-    if milestone_title == Milestone::Upcoming.name
-      milestone_title = Milestone::Upcoming.title
-    end
+    title =
+      case milestone_title
+      when Milestone::Upcoming.name then Milestone::Upcoming.title
+      when Milestone::Started.name then Milestone::Started.title
+      else milestone_title.presence
+      end
 
-    h(milestone_title.presence || default_label)
+    h(title || default_label)
+  end
+
+  def to_url_reference(issuable)
+    case issuable
+    when Issue
+      link_to issuable.to_reference, issue_url(issuable)
+    when MergeRequest
+      link_to issuable.to_reference, merge_request_url(issuable)
+    else
+      issuable.to_reference
+    end
   end
 
   def issuable_meta(issuable, project, text)
-    output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
+    output = content_tag(:strong, class: "identifier") do
+      concat("#{text} ")
+      concat(to_url_reference(issuable))
+    end
+
     output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
     output << content_tag(:strong) do
       author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
@@ -198,7 +218,7 @@ module IssuablesHelper
     @counts[issuable_type][state]
   end
 
-  IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+  IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
   private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
 
   def issuables_state_counter_cache_key(issuable_type, state)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index a2d21b67a775dae1822aa176594eaf7161588359..6978b0c89fd72736e635536d964995d1b78554eb 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -87,34 +87,6 @@ module IssuesHelper
     icon('eye-slash') if issue.confidential?
   end
 
-  def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
-    unicode ||= Gitlab::Emoji.emoji_filename(name) rescue ""
-
-    data = {
-      aliases: aliases.join(" "),
-      emoji: name,
-      unicode_name: unicode
-    }
-
-    if sprite
-      # Emoji icons for the emoji menu, these use a spritesheet.
-      content_tag :div, "",
-        class: "icon emoji-icon emoji-#{unicode}",
-        title: name,
-        data: data
-    else
-      # Emoji icons displayed separately, used for the awards already given
-      # to an issue or merge request.
-      content_tag :img, "",
-        class: "icon emoji",
-        title: name,
-        height: "20px",
-        width: "20px",
-        src: url_to_image("#{unicode}.png"),
-        data: data
-    end
-  end
-
   def award_user_list(awards, current_user, limit: 10)
     names = awards.map do |award|
       award.user == current_user ? 'You' : award.user.name
@@ -162,6 +134,20 @@ module IssuesHelper
     options_from_collection_for_select(options, 'name', 'title', params[:due_date])
   end
 
+  def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
+    link_text = merge_request.to_reference
+    link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
+
+    path = if single_discussion
+             Gitlab::UrlBuilder.build(single_discussion.first_note)
+           else
+             project = merge_request.project
+             namespace_project_merge_request_path(project.namespace, project, merge_request)
+           end
+
+    link_to link_text, path
+  end
+
   # Required for Banzai::Filter::IssueReferenceFilter
   module_function :url_for_issue
 end
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 320dd89c9d30f37b5010a83b0ac0814cea3fe7b2..68c09c922a67533eb826777cc85decdacfce2b7a 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -2,6 +2,7 @@ module JavascriptHelper
   def page_specific_javascript_tag(js)
     javascript_include_tag asset_path(js)
   end
+
   def page_specific_javascript_bundle_tag(js)
     javascript_include_tag(*webpack_asset_paths(js))
   end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
index 49ac12db832827dc1e4561be3691298aee0b2754..27ff4051c8d2025f77f779b27533bd19d0e807d5 100644
--- a/app/helpers/mattermost_helper.rb
+++ b/app/helpers/mattermost_helper.rb
@@ -1,9 +1,7 @@
 module MattermostHelper
   def mattermost_teams_options(teams)
-    teams_options = teams.map do |id, options|
-      [options['display_name'] || options['name'], id]
+    teams.map do |team|
+      [team['display_name'] || team['name'], team['id']]
     end
-
-    teams_options.compact.unshift(['Select team...', '0'])
   end
 end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 7d8505d704e7fab219c101a6a3967f81a43b9e24..38be073c8dcc2a5193b00844126cc88a37d92410 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -146,7 +146,7 @@ module MergeRequestsHelper
 
   def merge_params(merge_request)
     {
-      merge_when_build_succeeds: true,
+      merge_when_pipeline_succeeds: true,
       should_remove_source_branch: true,
       sha: merge_request.diff_head_sha
     }.merge(merge_params_ee(merge_request))
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 729928ce1ddfe29646e3f92e940df368ba4675fc..5053b937c022922376040b6960ec6a89d30480f1 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -82,12 +82,13 @@ module MilestonesHelper
   def milestone_remaining_days(milestone)
     if milestone.expired?
       content_tag(:strong, 'Past due')
-    elsif milestone.due_date
-      days    = milestone.remaining_days
-      content = content_tag(:strong, days)
-      content << " #{'day'.pluralize(days)} remaining"
     elsif milestone.upcoming?
       content_tag(:strong, 'Upcoming')
+    elsif milestone.due_date
+      time_ago = time_ago_in_words(milestone.due_date)
+      content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+      content.slice!("about ")
+      content << " remaining"
     elsif milestone.start_date && milestone.start_date.past?
       days    = milestone.elapsed_days
       content = content_tag(:strong, days)
@@ -97,7 +98,7 @@ module MilestonesHelper
 
   def milestone_date_range(milestone)
     if milestone.start_date && milestone.due_date
-      "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
+      "#{milestone.start_date.to_s(:medium)}–#{milestone.due_date.to_s(:medium)}"
     elsif milestone.due_date
       if milestone.due_date.past?
         "expired on #{milestone.due_date.to_s(:medium)}"
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index dc5ae8edbb2a9b64e150621b10cb572b3c8eecf2..2e3a15bc1b9ccbe97b73e09753c40c2a6408e2f5 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -33,7 +33,7 @@ module NamespacesHelper
   end
 
   def namespace_icon(namespace, size = 40)
-    if namespace.kind_of?(Group)
+    if namespace.is_a?(Group)
       group_icon(namespace)
     else
       avatar_icon(namespace.owner.email, size)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf87202f0e461c1f5cac57306123417..a8f167cbff268cc7b7b2734e33a270d450f2e052 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -16,6 +16,7 @@ module NavHelper
       "page-gutter build-sidebar right-sidebar-expanded"
     elsif current_path?('wikis#show') ||
         current_path?('wikis#edit') ||
+        current_path?('wikis#update') ||
         current_path?('wikis#history') ||
         current_path?('wikis#git_access')
       "page-gutter wiki-sidebar right-sidebar-expanded"
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c3a08d7631864b9cd7f81c233e56ad5331506c8c..243ef39ef61faf53d40cfcc9b8cc23413cdc7317 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -35,9 +35,8 @@ module PreferencesHelper
 
   def project_view_choices
     [
-      ['Readme (default)', :readme],
-      ['Activity view', :activity],
-      ['Files view', :files]
+      ['Files and Readme (default)', :files],
+      ['Activity', :activity]
     ]
   end
 
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb98204285d7c2df0bf9af990fe8201a3e9c01bd..bd0c2cd661e09f0be36e7be7b06ae06eee149fb1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -150,6 +150,22 @@ module ProjectsHelper
     ).html_safe
   end
 
+  def link_to_autodeploy_doc
+    link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank'
+  end
+
+  def autodeploy_flash_notice(branch_name)
+    "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \
+      choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe
+  end
+
+  def project_list_cache_key(project)
+    key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3']
+    key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
+
+    key
+  end
+
   private
 
   def repo_children_classes(field)
@@ -232,7 +248,7 @@ module ProjectsHelper
     when 'ssh'
       project.ssh_url_to_repo
     else
-      project.http_url_to_repo
+      project.http_url_to_repo(current_user)
     end
   end
 
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea5d2932ef42c08a9f7762c70c07e5fbdcf788ca
--- /dev/null
+++ b/app/helpers/rss_helper.rb
@@ -0,0 +1,5 @@
+module RssHelper
+  def rss_url_options
+    { format: :atom, private_token: current_user.try(:private_token) }
+  end
+end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index ff787fb4131480c8b958a39b82acc1d54b6c0bc6..959ee310867d72ca54bbead4133cae9eb0021975 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -16,7 +16,8 @@ module SortingHelper
       sort_value_oldest_signin => sort_title_oldest_signin,
       sort_value_downvotes => sort_title_downvotes,
       sort_value_upvotes => sort_title_upvotes,
-      sort_value_priority => sort_title_priority
+      sort_value_priority => sort_title_priority,
+      sort_value_label_priority => sort_title_label_priority
     }
   end
 
@@ -30,7 +31,7 @@ module SortingHelper
     }
 
     if current_controller?('admin/projects')
-      options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+      options[sort_value_largest_repo] = sort_title_largest_repo
     end
 
     options
@@ -53,6 +54,10 @@ module SortingHelper
     'Priority'
   end
 
+  def sort_title_label_priority
+    'Label priority'
+  end
+
   def sort_title_oldest_updated
     'Oldest updated'
   end
@@ -161,6 +166,10 @@ module SortingHelper
     'priority'
   end
 
+  def sort_value_label_priority
+    'label_priority'
+  end
+
   def sort_value_oldest_updated
     'updated_asc'
   end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 9a748aaaf338104131131944c686c6c2142bbdd6..fb95f2b565ef0ae564a5aeba310a4872fc12142f 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -37,8 +37,8 @@ module SubmoduleHelper
   end
 
   def self_url?(url, namespace, project)
-    return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/',
-                            project, '.git' ].join('')
+    return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
+                           project, '.git'].join('')
     url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
   end
 
@@ -48,8 +48,8 @@ module SubmoduleHelper
   end
 
   def standard_links(host, namespace, project, commit)
-    base = [ 'https://', host, '/', namespace, '/', project ].join('')
-    [base, [ base, '/tree/', commit ].join('')]
+    base = ['https://', host, '/', namespace, '/', project].join('')
+    [base, [base, '/tree/', commit].join('')]
   end
 
   def relative_self_links(url, commit)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 547f62589097198fd112d4ca26033ea15e2903e0..1a55ee05996e232172812833488b2d453e2c4747 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -99,7 +99,7 @@ module TabHelper
       return 'active'
     end
 
-    if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
+    if %w(services hooks deploy_keys protected_branches).include? controller.controller_name
       "active"
     end
   end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index c52afd6db1c286af1985750a9472fa8471059c96..847a8fdfca676b02f3ea6a1c0f52325d0a2c1b98 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -39,9 +39,13 @@ module TodosHelper
       namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
                                     todo.target, anchor: anchor)
     else
-      path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
-
-      path.unshift(:pipelines) if todo.build_failed?
+      if todo.build_failed?
+        # associated namespace and route would be loaded from the db again if todo.project was used
+        project = todo.target.project
+        path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
+      else
+        path = [todo.target]
+      end
 
       polymorphic_path(path, anchor: anchor)
     end
@@ -99,8 +103,7 @@ module TodosHelper
   end
 
   def todo_projects_options
-    projects = current_user.authorized_projects.sorted_by_activity.non_archived
-    projects = projects.includes(:namespace)
+    projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
 
     projects = projects.map do |project|
       { id: project.id, text: project.name_with_namespace }
@@ -150,6 +153,6 @@ module TodosHelper
   private
 
   def show_todo_state?(todo)
-    (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state)
+    (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
   end
 end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index b0135ea2e95696897a99c566ebf0bc5abb22e953..a48d4475e979cde0f4e8035a360ebe4217decd6b 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,9 +1,9 @@
 module TriggersHelper
   def builds_trigger_url(project_id, ref: nil)
     if ref.nil?
-      "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+      "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline"
     else
-      "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+      "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline"
     end
   end
 
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index fc93acfe63e09870ba45cf2dbd540c9fbe45b81a..169cedeb796e041f81e1a8e41600210dc189c4d3 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -89,13 +89,9 @@ module VisibilityLevelHelper
     current_application_settings.restricted_visibility_levels || []
   end
 
-  def default_project_visibility
-    current_application_settings.default_project_visibility
-  end
-
-  def default_group_visibility
-    current_application_settings.default_group_visibility
-  end
+  delegate  :default_project_visibility,
+            :default_group_visibility,
+            to: :current_application_settings
 
   def skip_level?(form_model, level)
     form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201ac9509fc963581f2e586ba91efa727..0000000000000000000000000000000000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
-  module Builds
-    def build_fail_email(build_id, to)
-      @build = Ci::Build.find(build_id)
-      @project = @build.project
-
-      add_project_headers
-      add_build_headers('failed')
-
-      mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
-    end
-
-    def build_success_email(build_id, to)
-      @build = Ci::Build.find(build_id)
-      @project = @build.project
-
-      add_project_headers
-      add_build_headers('success')
-      mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
-    end
-
-    private
-
-    def add_build_headers(status)
-      headers['X-GitLab-Build-Id'] = @build.id
-      headers['X-GitLab-Build-Ref'] = @build.ref
-      headers['X-GitLab-Build-Status'] = status.to_s
-    end
-  end
-end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 9460a6cd2be9bc2edf15e180d2316313380303b8..f9f45ab987b39c08429f59eed603ec752e6521d2 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -22,8 +22,8 @@ module Emails
       mail(bcc: recipients,
            subject: pipeline_subject(status),
            skip_premailer: true) do |format|
-        format.html { render layout: false }
-        format.text
+        format.html { render layout: 'mailer' }
+        format.text { render layout: 'mailer' }
       end
     end
 
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81a51a69c6fbeb4b39e36d08284996c..14df6f8f0a3cd7f2fc06ec811dcf895a8d216995 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
   include Emails::Notes
   include Emails::Projects
   include Emails::Profile
-  include Emails::Builds
   include Emails::Pipelines
   include Emails::Members
 
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 21db2fe04a037fa19a238969034ac12e075ee18f..22a9f5da646fe3ec64c9929b1bae41f52c6121a2 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -1,10 +1,11 @@
 class RepositoryCheckMailer < BaseMailer
   def notify(failed_count)
-    if failed_count == 1
-      @message = "One project failed its last repository check"
-    else
-      @message = "#{failed_count} projects failed their last repository check"
-    end
+    @message =
+      if failed_count == 1
+        "One project failed its last repository check"
+      else
+        "#{failed_count} projects failed their last repository check"
+      end
 
     mail(
       to: User.admins.pluck(:email),
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ad6c588202e531d0ab2759b9211293d562d62e12..f3692a5a06764f60a1a28ed1ee6bcfdc64dd751b 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -56,15 +56,16 @@ class Ability
       end
     end
 
-    def allowed?(user, action, subject)
+    def allowed?(user, action, subject = :global)
       allowed(user, subject).include?(action)
     end
 
-    def allowed(user, subject)
+    def allowed(user, subject = :global)
+      return BasePolicy::RuleSet.none if subject.nil?
       return uncached_allowed(user, subject) unless RequestStore.active?
 
       user_key = user ? user.id : 'anonymous'
-      subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+      subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}"
       key = "/ability/#{user_key}/#{subject_key}"
       RequestStore[key] ||= uncached_allowed(user, subject).freeze
     end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index e4106e1c2e90f1d13807e23227071920ce0c57a8..c79326e84275ae1c5bc325076ca7db2c5567dc97 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base
 
   mount_uploader :logo,         AttachmentUploader
   mount_uploader :header_logo,  AttachmentUploader
+  has_many :uploads, as: :model, dependent: :destroy
 end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b94a71e1ea7f34a257c10f264f12a9e523944aaa..9d01a70c77dee5a06e8b721394f177fc8ba28f6b 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -6,7 +6,7 @@ class ApplicationSetting < ActiveRecord::Base
   add_authentication_token_field :health_check_access_token
   add_authentication_token_field :container_registry_access_token
 
-  CACHE_KEY = 'application_setting.last'
+  CACHE_KEY = 'application_setting.last'.freeze
   DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s*     # comma or semicolon, optionally surrounded by whitespace
                             |               # or
                             \s              # any whitespace character
@@ -65,6 +65,16 @@ class ApplicationSetting < ActiveRecord::Base
             presence: true,
             if: :akismet_enabled
 
+  validates :unique_ips_limit_per_user,
+            numericality: { greater_than_or_equal_to: 1 },
+            presence: true,
+            if: :unique_ips_limit_enabled
+
+  validates :unique_ips_limit_time_window,
+            numericality: { greater_than_or_equal_to: 0 },
+            presence: true,
+            if: :unique_ips_limit_enabled
+
   validates :koding_url,
             presence: true,
             if: :koding_enabled
@@ -77,6 +87,12 @@ class ApplicationSetting < ActiveRecord::Base
             presence: true,
             numericality: { only_integer: true, greater_than: 0 }
 
+  validates :max_artifacts_size,
+            presence: true,
+            numericality: { only_integer: true, greater_than: 0 }
+
+  validates :default_artifacts_expire_in, presence: true, duration: true
+
   validates :container_registry_token_expire_delay,
             presence: true,
             numericality: { only_integer: true, greater_than: 0 }
@@ -149,6 +165,8 @@ class ApplicationSetting < ActiveRecord::Base
   end
 
   def self.current
+    ensure_cache_setup
+
     Rails.cache.fetch(CACHE_KEY) do
       ApplicationSetting.last
     end
@@ -162,22 +180,34 @@ class ApplicationSetting < ActiveRecord::Base
   end
 
   def self.cached
+    ensure_cache_setup
     Rails.cache.fetch(CACHE_KEY)
   end
 
+  def self.ensure_cache_setup
+    # This is a workaround for a Rails bug that causes attribute methods not
+    # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+    ApplicationSetting.define_attribute_methods
+  end
+
   def self.defaults_ce
     {
       after_sign_up_text: nil,
       akismet_enabled: false,
       container_registry_token_expire_delay: 5,
+      default_artifacts_expire_in: '30 days',
       default_branch_protection: Settings.gitlab['default_branch_protection'],
       default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       default_projects_limit: Settings.gitlab['default_projects_limit'],
       default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+      default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       disabled_oauth_sign_in_sources: [],
       domain_whitelist: Settings.gitlab['domain_whitelist'],
       gravatar_enabled: Settings.gravatar['enabled'],
       help_page_text: nil,
+      unique_ips_limit_per_user: 10,
+      unique_ips_limit_time_window: 3600,
+      unique_ips_limit_enabled: false,
       housekeeping_bitmaps_enabled: true,
       housekeeping_enabled: true,
       housekeeping_full_repack_period: 50,
@@ -203,9 +233,9 @@ class ApplicationSetting < ActiveRecord::Base
       sign_in_text: nil,
       signin_enabled: Settings.gitlab['signin_enabled'],
       signup_enabled: Settings.gitlab['signup_enabled'],
+      terminal_max_session_time: 0,
       two_factor_grace_period: 48,
-      user_default_external: false,
-      terminal_max_session_time: 0
+      user_default_external: false
     }
   end
 
@@ -217,6 +247,14 @@ class ApplicationSetting < ActiveRecord::Base
     create(defaults)
   end
 
+  def self.human_attribute_name(attr, _options = {})
+    if attr == :default_artifacts_expire_in
+      'Default artifacts expiration'
+    else
+      super
+    end
+  end
+
   def home_page_url_column_exist
     ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
   end
@@ -264,6 +302,22 @@ class ApplicationSetting < ActiveRecord::Base
     self.repository_storages = [value]
   end
 
+  def default_project_visibility=(level)
+    super(Gitlab::VisibilityLevel.level_value(level))
+  end
+
+  def default_snippet_visibility=(level)
+    super(Gitlab::VisibilityLevel.level_value(level))
+  end
+
+  def default_group_visibility=(level)
+    super(Gitlab::VisibilityLevel.level_value(level))
+  end
+
+  def restricted_visibility_levels=(levels)
+    super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
+  end
+
   # Choose one of the available repository storage options. Currently all have
   # equal weighting.
   def pick_repository_storage
diff --git a/app/models/blob.rb b/app/models/blob.rb
index ab92e82033544a8db5236490f81199f04c42ea96..1376b86fdad4bb852bdd26259fc64e82fdc12d9b 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -54,9 +54,13 @@ class Blob < SimpleDelegator
     UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
   end
 
-  def to_partial_path
+  def to_partial_path(project)
     if lfs_pointer?
-      'download'
+      if project.lfs_enabled?
+        'download'
+      else
+        'text'
+      end
     elsif image? || svg?
       'image'
     elsif text?
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c52b6f1591377e8af025068cd98d7e7b6bb4bd0c
--- /dev/null
+++ b/app/models/chat_team.rb
@@ -0,0 +1,6 @@
+class ChatTeam < ActiveRecord::Base
+  validates :team_id, presence: true
+  validates :namespace, uniqueness: true
+
+  belongs_to :namespace
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e018f8e7c4eadda5a8cae61642fc5b71d875a257..ad0be70c32a4c6d939e7fa1c71992b0ae9c48707 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,15 +15,17 @@ module Ci
     def persisted_environment
       @persisted_environment ||= Environment.find_by(
         name: expanded_environment_name,
-        project_id: gl_project_id
+        project: project
       )
     end
 
     serialize :options
     serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
 
+    delegate :name, to: :project, prefix: true
+
     validates :coverage, numericality: true, allow_blank: true
-    validates_presence_of :ref
+    validates :ref, presence: true
 
     scope :unstarted, ->() { where(runner_id: nil) }
     scope :ignore_failures, ->() { where(allow_failure: false) }
@@ -53,15 +55,6 @@ module Ci
         pending.unstarted.order('created_at ASC').first
       end
 
-      def create_from(build)
-        new_build = build.dup
-        new_build.status = 'pending'
-        new_build.runner_id = nil
-        new_build.trigger_request_id = nil
-        new_build.token = nil
-        new_build.save
-      end
-
       def retry(build, current_user)
         Ci::RetryBuildService
           .new(build.project, current_user)
@@ -70,6 +63,10 @@ module Ci
     end
 
     state_machine :status do
+      event :actionize do
+        transition created: :manual
+      end
+
       after_transition any => [:pending] do |build|
         build.run_after_commit do
           BuildQueueWorker.perform_async(id)
@@ -101,16 +98,21 @@ module Ci
         .fabricate!
     end
 
-    def manual?
-      self.when == 'manual'
-    end
-
     def other_actions
       pipeline.manual_actions.where.not(name: name)
     end
 
     def playable?
-      project.builds_enabled? && commands.present? && manual? && skipped?
+      project.builds_enabled? && has_commands? &&
+        action? && manual?
+    end
+
+    def action?
+      self.when == 'manual'
+    end
+
+    def has_commands?
+      commands.present?
     end
 
     def play(current_user)
@@ -129,7 +131,7 @@ module Ci
     end
 
     def retryable?
-      project.builds_enabled? && commands.present? &&
+      project.builds_enabled? && has_commands? &&
         (success? || failed? || canceled?)
     end
 
@@ -221,7 +223,8 @@ module Ci
 
     def merge_request
       merge_requests = MergeRequest.includes(:merge_request_diff)
-                                   .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+                                   .where(source_branch: ref,
+                                          source_project: pipeline.project)
                                    .reorder(iid: :asc)
 
       merge_requests.find do |merge_request|
@@ -229,14 +232,6 @@ module Ci
       end
     end
 
-    def project_id
-      gl_project_id
-    end
-
-    def project_name
-      project.name
-    end
-
     def repo_url
       auth = "gitlab-ci-token:#{ensure_token!}@"
       project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -257,7 +252,7 @@ module Ci
       return unless regex
 
       matches = text.scan(Regexp.new(regex)).last
-      matches = matches.last if matches.kind_of?(Array)
+      matches = matches.last if matches.is_a?(Array)
       coverage = matches.gsub(/\d+(\.\d+)?/).first
 
       if coverage.present?
@@ -486,7 +481,7 @@ module Ci
     def artifacts_expire_in=(value)
       self.artifacts_expire_at =
         if value
-          Time.now + ChronicDuration.parse(value)
+          ChronicDuration.parse(value)&.seconds&.from_now
         end
     end
 
@@ -519,10 +514,41 @@ module Ci
       ]
     end
 
+    def steps
+      [Gitlab::Ci::Build::Step.from_commands(self),
+       Gitlab::Ci::Build::Step.from_after_script(self)].compact
+    end
+
+    def image
+      Gitlab::Ci::Build::Image.from_image(self)
+    end
+
+    def services
+      Gitlab::Ci::Build::Image.from_services(self)
+    end
+
+    def artifacts
+      [options[:artifacts]]
+    end
+
+    def cache
+      [options[:cache]]
+    end
+
     def credentials
       Gitlab::Ci::Build::Credentials::Factory.new(self).create!
     end
 
+    def dependencies
+      depended_jobs = depends_on_builds
+
+      return depended_jobs unless options[:dependencies].present?
+
+      depended_jobs.select do |job|
+        options[:dependencies].include?(job.name)
+      end
+    end
+
     private
 
     def update_artifacts_size
@@ -542,13 +568,38 @@ module Ci
     end
 
     def unscoped_project
-      @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+      @unscoped_project ||= Project.unscoped.find_by(id: project_id)
     end
 
+    CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+
     def predefined_variables
       variables = [
         { key: 'CI', value: 'true', public: true },
         { key: 'GITLAB_CI', value: 'true', public: true },
+        { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+        { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+        { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+        { key: 'CI_JOB_ID', value: id.to_s, public: true },
+        { key: 'CI_JOB_NAME', value: name, public: true },
+        { key: 'CI_JOB_STAGE', value: stage, public: true },
+        { key: 'CI_JOB_TOKEN', value: token, public: false },
+        { key: 'CI_COMMIT_SHA', value: sha, public: true },
+        { key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
+        { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
+        { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
+        { key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
+        { key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
+      ]
+
+      variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
+      variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
+      variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
+      variables.concat(legacy_variables)
+    end
+
+    def legacy_variables
+      variables = [
         { key: 'CI_BUILD_ID', value: id.to_s, public: true },
         { key: 'CI_BUILD_TOKEN', value: token, public: false },
         { key: 'CI_BUILD_REF', value: sha, public: true },
@@ -556,14 +607,12 @@ module Ci
         { 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 },
-        { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
-        { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
+        { key: 'CI_BUILD_STAGE', value: stage, public: true }
       ]
-      variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
-      variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
-      variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual?
+
+      variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
+      variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
+      variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
       variables
     end
 
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index dc4590a9923acef1bd418337d98bf431888e9475..f12be98c80c0bf82f61fd1b09e2f2d254116725b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,21 +5,22 @@ module Ci
     include Importable
     include AfterCommitQueue
 
-    self.table_name = 'ci_commits'
-
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
     belongs_to :user
 
     has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
     has_many :builds, foreign_key: :commit_id
     has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
 
-    validates_presence_of :sha, unless: :importing?
-    validates_presence_of :ref, unless: :importing?
-    validates_presence_of :status, unless: :importing?
+    delegate :id, to: :project, prefix: true
+
+    validates :sha, presence: { unless: :importing? }
+    validates :ref, presence: { unless: :importing? }
+    validates :status, presence: { unless: :importing? }
     validate :valid_commit_sha, unless: :importing?
 
     after_create :keep_around_commits, unless: :importing?
+    after_create :refresh_build_status_cache
 
     state_machine :status, initial: :created do
       event :enqueue do
@@ -47,6 +48,10 @@ module Ci
         transition any - [:canceled] => :canceled
       end
 
+      event :block do
+        transition any - [:manual] => :manual
+      end
+
       # IMPORTANT
       # Do not add any operations to this state_machine
       # Create a separate worker for each new operation
@@ -93,8 +98,11 @@ module Ci
         .select("max(#{quoted_table_name}.id)")
         .group(:ref, :sha)
 
-      relation = ref ? where(ref: ref) : self
-      relation.where(id: max_id)
+      if ref
+        where(ref: ref, id: max_id.where(ref: ref))
+      else
+        where(id: max_id)
+      end
     end
 
     def self.latest_status(ref = nil)
@@ -105,6 +113,12 @@ module Ci
       success.latest(ref).order(id: :desc).first
     end
 
+    def self.latest_successful_for_refs(refs)
+      success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+        hash[pipeline.ref] ||= pipeline
+      end
+    end
+
     def self.truncate_sha(sha)
       sha[0...8]
     end
@@ -135,7 +149,7 @@ module Ci
 
       status_sql = statuses.latest.where('stage=sg.stage').status_sql
 
-      warnings_sql = statuses.latest.select('COUNT(*) > 0')
+      warnings_sql = statuses.latest.select('COUNT(*)')
         .where('stage=sg.stage').failed_but_allowed.to_sql
 
       stages_with_statuses = CommitStatus.from(stages_query, :sg)
@@ -150,10 +164,6 @@ module Ci
       builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
     end
 
-    def project_id
-      project.id
-    end
-
     # For now the only user who participates is the user who triggered
     def participants(_current_user = nil)
       Array(user)
@@ -320,8 +330,10 @@ module Ci
         when 'failed' then drop
         when 'canceled' then cancel
         when 'skipped' then skip
+        when 'manual' then block
         end
       end
+      refresh_build_status_cache
     end
 
     def predefined_variables
@@ -363,6 +375,10 @@ module Ci
         .fabricate!
     end
 
+    def refresh_build_status_cache
+      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
+    end
+
     private
 
     def pipeline_data
diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb
new file mode 100644
index 0000000000000000000000000000000000000000..048047d0e346ecbb08458e6b57043e7b409cf486
--- /dev/null
+++ b/app/models/ci/pipeline_status.rb
@@ -0,0 +1,86 @@
+# This class is not backed by a table in the main database.
+# It loads the latest Pipeline for the HEAD of a repository, and caches that
+# in Redis.
+module Ci
+  class PipelineStatus
+    attr_accessor :sha, :status, :project, :loaded
+
+    delegate :commit, to: :project
+
+    def self.load_for_project(project)
+      new(project).tap do |status|
+        status.load_status
+      end
+    end
+
+    def initialize(project, sha: nil, status: nil)
+      @project = project
+      @sha = sha
+      @status = status
+    end
+
+    def has_status?
+      loaded? && sha.present? && status.present?
+    end
+
+    def load_status
+      return if loaded?
+
+      if has_cache?
+        load_from_cache
+      else
+        load_from_commit
+        store_in_cache
+      end
+
+      self.loaded = true
+    end
+
+    def load_from_commit
+      return unless commit
+
+      self.sha = commit.sha
+      self.status = commit.status
+    end
+
+    # We only cache the status for the HEAD commit of a project
+    # This status is rendered in project lists
+    def store_in_cache_if_needed
+      return unless sha
+      return delete_from_cache unless commit
+      store_in_cache if commit.sha == self.sha
+    end
+
+    def load_from_cache
+      Gitlab::Redis.with do |redis|
+        self.sha, self.status = redis.hmget(cache_key, :sha, :status)
+      end
+    end
+
+    def store_in_cache
+      Gitlab::Redis.with do |redis|
+        redis.mapped_hmset(cache_key, { sha: sha, status: status })
+      end
+    end
+
+    def delete_from_cache
+      Gitlab::Redis.with do |redis|
+        redis.del(cache_key)
+      end
+    end
+
+    def has_cache?
+      Gitlab::Redis.with do |redis|
+        redis.exists(cache_key)
+      end
+    end
+
+    def loaded?
+      self.loaded
+    end
+
+    def cache_key
+      "projects/#{project.id}/build_status"
+    end
+  end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 07a086b0acae00b1a48cfdcad69cdf03572bb764..487ba61bc9c7019618cf8a25f5c60961c8b3761f 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,12 +4,12 @@ module Ci
 
     RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
     LAST_CONTACT_TIME = 1.hour.ago
-    AVAILABLE_SCOPES = %w[specific shared active paused online]
-    FORM_EDITABLE = %i[description tag_list active run_untagged locked]
+    AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
+    FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze
 
     has_many :builds
     has_many :runner_projects, dependent: :destroy
-    has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+    has_many :projects, through: :runner_projects
 
     has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
 
@@ -24,7 +24,7 @@ module Ci
 
     scope :owned_or_shared, ->(project_id) do
       joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
-        .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+        .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
     end
 
     scope :assignable_for, ->(project) do
@@ -127,18 +127,15 @@ module Ci
 
     def tick_runner_queue
       SecureRandom.hex.tap do |new_update|
-        Gitlab::Redis.with do |redis|
-          redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
-        end
+        ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
+          expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true)
       end
     end
 
     def ensure_runner_queue_value
-      Gitlab::Redis.with do |redis|
-        value = SecureRandom.hex
-        redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
-        redis.get(runner_queue_key)
-      end
+      new_value = SecureRandom.hex
+      ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value,
+        expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
     end
 
     def is_runner_queue_value_latest?(value)
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 1f9baeca5b17250bc4f89cb343daa5e0b3133ea9..5f01a0daae917a85faed929d3e5c7c18db789e4d 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
 module Ci
   class RunnerProject < ActiveRecord::Base
     extend Ci::Model
-    
+
     belongs_to :runner
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
 
-    validates_uniqueness_of :runner_id, scope: :gl_project_id
+    validates :runner_id, uniqueness: { scope: :project_id }
   end
 end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index ca74c91b0627b5861e8052b0c94c31a081c22ee1..e7d6b17d445ef21782166ecb74b491426251d1d1 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -46,10 +46,10 @@ module Ci
     end
 
     def has_warnings?
-      if @warnings.nil?
-        statuses.latest.failed_but_allowed.any?
+      if @warnings.is_a?(Integer)
+        @warnings > 0
       else
-        @warnings
+        statuses.latest.failed_but_allowed.any?
       end
     end
   end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 62889fe80d8c4045d38d55046b6733f057774fc0..cba1d81a8616d2c0bcab9308ae91b6534eb1a000 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,11 +4,12 @@ module Ci
 
     acts_as_paranoid
 
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
+    belongs_to :owner, class_name: "User"
+
     has_many :trigger_requests, dependent: :destroy
 
-    validates_presence_of :token
-    validates_uniqueness_of :token
+    validates :token, presence: true, uniqueness: true
 
     before_validation :set_default_values
 
@@ -25,7 +26,15 @@ module Ci
     end
 
     def short_token
-      token[0...10]
+      token[0...4]
+    end
+
+    def legacy?
+      self.owner_id.blank?
+    end
+
+    def can_access_project?
+      self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
     end
   end
 end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5de8d4f2605641a1dc10185cb751402..6c6586110c5e98ae54637f43a335419f86931bf6 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
   class Variable < ActiveRecord::Base
     extend Ci::Model
 
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
 
     validates :key,
       presence: true,
-      uniqueness: { scope: :gl_project_id },
+      uniqueness: { scope: :project_id },
       length: { maximum: 255 },
       format: { with: /\A[a-zA-Z0-9_]+\z/,
                 message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 46f06733da198111f9a31f2f8dd4ff7fbdb25941..ce92cc369ad2576bfe01470dee660af955468789 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,12 +22,12 @@ class Commit
   DIFF_HARD_LIMIT_LINES = 50000
 
   # The SHA can be between 7 and 40 hex characters.
-  COMMIT_SHA_PATTERN = '\h{7,40}'
+  COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
 
   class << self
     def decorate(commits, project)
       commits.map do |commit|
-        if commit.kind_of?(Commit)
+        if commit.is_a?(Commit)
           commit
         else
           self.new(commit, project)
@@ -105,7 +105,7 @@ class Commit
   end
 
   def diff_line_count
-    @diff_line_count ||= Commit::diff_line_count(raw_diffs)
+    @diff_line_count ||= Commit.diff_line_count(raw_diffs)
     @diff_line_count
   end
 
@@ -122,11 +122,12 @@ class Commit
   def full_title
     return @full_title if @full_title
 
-    if safe_message.blank?
-      @full_title = no_commit_message
-    else
-      @full_title = safe_message.split("\n", 2).first
-    end
+    @full_title =
+      if safe_message.blank?
+        no_commit_message
+      else
+        safe_message.split("\n", 2).first
+      end
   end
 
   # Returns the commits description
@@ -230,6 +231,10 @@ class Commit
     project.pipelines.where(sha: sha)
   end
 
+  def latest_pipeline
+    pipelines.last
+  end
+
   def status(ref = nil)
     @statuses ||= {}
 
@@ -316,7 +321,14 @@ class Commit
   end
 
   def raw_diffs(*args)
-    raw.diffs(*args)
+    use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+    deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+    if use_gitaly && !deltas_only
+      Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+    else
+      raw.diffs(*args)
+    end
   end
 
   def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 99a6326309d0217056af2e45e2687ff4b05b1d61..8c71267da65d04d7a71024bd921691582526bc86 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,15 +5,16 @@ class CommitStatus < ActiveRecord::Base
 
   self.table_name = 'ci_builds'
 
-  belongs_to :project, foreign_key: :gl_project_id
+  belongs_to :project
   belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :user
 
   delegate :commit, to: :pipeline
+  delegate :sha, :short_sha, to: :pipeline
 
   validates :pipeline, presence: true, unless: :importing?
 
-  validates_presence_of :name
+  validates :name, presence: true
 
   alias_attribute :author, :user
 
@@ -28,9 +29,11 @@ class CommitStatus < ActiveRecord::Base
   end
 
   scope :exclude_ignored, -> do
-    # We want to ignore failed_but_allowed jobs
+    # We want to ignore failed but allowed to fail jobs.
+    #
+    # TODO, we also skip ignored optional manual actions.
     where("allow_failure = ? OR status IN (?)",
-      false, all_state_names - [:failed, :canceled])
+      false, all_state_names - [:failed, :canceled, :manual])
   end
 
   scope :retried, -> { where.not(id: latest) }
@@ -41,11 +44,11 @@ class CommitStatus < ActiveRecord::Base
 
   state_machine :status do
     event :enqueue do
-      transition [:created, :skipped] => :pending
+      transition [:created, :skipped, :manual] => :pending
     end
 
     event :process do
-      transition skipped: :created
+      transition [:skipped, :manual] => :created
     end
 
     event :run do
@@ -65,7 +68,7 @@ class CommitStatus < ActiveRecord::Base
     end
 
     event :cancel do
-      transition [:created, :pending, :running] => :canceled
+      transition [:created, :pending, :running, :manual] => :canceled
     end
 
     before_transition created: [:pending, :running] do |commit_status|
@@ -85,7 +88,7 @@ class CommitStatus < ActiveRecord::Base
 
       commit_status.run_after_commit do
         pipeline.try do |pipeline|
-          if complete?
+          if complete? || manual?
             PipelineProcessWorker.perform_async(pipeline.id)
           else
             PipelineUpdateWorker.perform_async(pipeline.id)
@@ -102,8 +105,6 @@ class CommitStatus < ActiveRecord::Base
     end
   end
 
-  delegate :sha, :short_sha, to: :pipeline
-
   def before_sha
     pipeline.before_sha || Gitlab::Git::BLANK_SHA
   end
@@ -132,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
     false
   end
 
+  # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+  # and prior.
+  def gl_project_id
+    'dummy'
+  end
+
   def detailed_status(current_user)
     Gitlab::Ci::Status::Factory
       .new(self, current_user)
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 073ac4c1b65ef43ae2fb8ad8711aad8f629b87c9..a7fd0a15f0fa0a6673fc5b86b3333d1c644d61e1 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -101,6 +101,6 @@ module Awardable
   private
 
   def normalize_name(name)
-    Gitlab::AwardEmoji.normalize_emoji_name(name)
+    Gitlab::Emoji.normalize_emoji_name(name)
   end
 end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index a600f9c14c5a5dd17ad4f029b2eb04ac4da8c9fd..8ea95beed798a97be1b2b50b3465efaf785cde1b 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,14 +11,15 @@ module CacheMarkdownField
   # Knows about the relationship between markdown and html field names, and
   # stores the rendering contexts for the latter
   class FieldData
-    extend Forwardable
-
     def initialize
       @data = {}
     end
 
-    def_delegators :@data, :[], :[]=
-    def_delegator :@data, :keys, :markdown_fields
+    delegate :[], :[]=, to: :@data
+
+    def markdown_fields
+      @data.keys
+    end
 
     def html_field(markdown_field)
       "#{markdown_field}_html"
@@ -45,7 +46,7 @@ module CacheMarkdownField
     Project
     Release
     Snippet
-  ]
+  ].freeze
 
   def self.caching_classes
     CACHING_CLASSES.map(&:constantize)
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index fe0cea8465f628842bfe311f9bcdcbdce76d93f2..034e9f40ff09764e8add024e27e08ae72c803a3c 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -13,11 +13,12 @@ module CaseSensitivity
       params.each do |key, value|
         column = ActiveRecord::Base.connection.quote_table_name(key)
 
-        if cast_lower
-          condition = "LOWER(#{column}) = LOWER(:value)"
-        else
-          condition = "#{column} = :value"
-        end
+        condition =
+          if cast_lower
+            "LOWER(#{column}) = LOWER(:value)"
+          else
+            "#{column} = :value"
+          end
 
         criteria = criteria.where(condition, value: value)
       end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 431c035496917aef14665c8d925bd487d0d8f3e5..0a1a65da05a09775502ca06f9d6fe0dd8585d6fb 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,23 +1,22 @@
 module HasStatus
   extend ActiveSupport::Concern
 
-  DEFAULT_STATUS = 'created'
-  AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
-  STARTED_STATUSES = %w[running success failed skipped]
-  ACTIVE_STATUSES = %w[pending running]
-  COMPLETED_STATUSES = %w[success failed canceled skipped]
-  ORDERED_STATUSES = %w[failed pending running canceled success skipped]
+  DEFAULT_STATUS = 'created'.freeze
+  BLOCKED_STATUS = 'manual'.freeze
+  AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze
+  STARTED_STATUSES = %w[running success failed skipped manual].freeze
+  ACTIVE_STATUSES = %w[pending running].freeze
+  COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
+  ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
 
   class_methods do
     def status_sql
-      scope = if respond_to?(:exclude_ignored)
-                exclude_ignored
-              else
-                all
-              end
+      scope = respond_to?(:exclude_ignored) ? exclude_ignored : all
+
       builds = scope.select('count(*)').to_sql
       created = scope.created.select('count(*)').to_sql
       success = scope.success.select('count(*)').to_sql
+      manual = scope.manual.select('count(*)').to_sql
       pending = scope.pending.select('count(*)').to_sql
       running = scope.running.select('count(*)').to_sql
       skipped = scope.skipped.select('count(*)').to_sql
@@ -30,7 +29,9 @@ module HasStatus
         WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
         WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
         WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
-        WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
+        WHEN (#{running})+(#{pending})>0 THEN 'running'
+        WHEN (#{manual})>0 THEN 'manual'
+        WHEN (#{created})>0 THEN 'running'
         ELSE 'failed'
       END)"
     end
@@ -63,6 +64,7 @@ module HasStatus
       state :success, value: 'success'
       state :canceled, value: 'canceled'
       state :skipped, value: 'skipped'
+      state :manual, value: 'manual'
     end
 
     scope :created, -> { where(status: 'created') }
@@ -73,12 +75,13 @@ module HasStatus
     scope :failed, -> { where(status: 'failed')  }
     scope :canceled, -> { where(status: 'canceled')  }
     scope :skipped, -> { where(status: 'skipped')  }
+    scope :manual, -> { where(status: 'manual')  }
     scope :running_or_pending, -> { where(status: [:running, :pending]) }
     scope :finished, -> { where(status: [:success, :failed, :canceled]) }
     scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
 
     scope :cancelable, -> do
-      where(status: [:running, :pending, :created])
+      where(status: [:running, :pending, :created, :manual])
     end
   end
 
@@ -94,6 +97,10 @@ module HasStatus
     COMPLETED_STATUSES.include?(status)
   end
 
+  def blocked?
+    BLOCKED_STATUS == status
+  end
+
   private
 
   def calculate_duration
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5f53c48fc88bdb2d1c6a7e0e6de4de7de132751f..e7bd20b322ab633d3cda1f2169f6875505b71d31 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -16,9 +16,9 @@ module Issuable
   include TimeTrackable
 
   # This object is used to gather issuable meta data for displaying
-  # upvotes, downvotes and notes count for issues and merge requests
+  # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
   # lists avoiding n+1 queries and improving performance.
-  IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
+  IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count)
 
   included do
     cache_markdown_field :title, pipeline: :single_line
@@ -46,12 +46,26 @@ module Issuable
 
     has_one :metrics
 
+    delegate :name,
+             :email,
+             :public_email,
+             to: :author,
+             prefix: true
+
+    delegate :name,
+             :email,
+             :public_email,
+             to: :assignee,
+             allow_nil: true,
+             prefix: true
+
     validates :author, presence: true
     validates :title, presence: true, length: { maximum: 255 }
 
     scope :authored, ->(user) { where(author_id: user) }
     scope :assigned_to, ->(u) { where(assignee_id: u.id)}
     scope :recent, -> { reorder(id: :desc) }
+    scope :order_position_asc, -> { reorder(position: :asc) }
     scope :assigned, -> { where("assignee_id IS NOT NULL") }
     scope :unassigned, -> { where("assignee_id IS NULL") }
     scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -68,21 +82,10 @@ module Issuable
 
     scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
     scope :join_project, -> { joins(:project) }
-    scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) }
+    scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) }
     scope :references_project, -> { references(:project) }
     scope :non_archived, -> { join_project.where(projects: { archived: false }) }
 
-    delegate :name,
-             :email,
-             to: :author,
-             prefix: true
-
-    delegate :name,
-             :email,
-             to: :assignee,
-             allow_nil: true,
-             prefix: true
-
     attr_mentionable :title, pipeline: :single_line
     attr_mentionable :description
 
@@ -143,7 +146,9 @@ module Issuable
                when 'milestone_due_desc' then order_milestone_due_desc
                when 'downvotes_desc' then order_downvotes_desc
                when 'upvotes_desc' then order_upvotes_desc
-               when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
+               when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+               when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
+               when 'position_asc' then  order_position_asc
                else
                  order_by(method)
                end
@@ -152,7 +157,28 @@ module Issuable
       sorted.order(id: :desc)
     end
 
-    def order_labels_priority(excluded_labels: [])
+    def order_due_date_and_labels_priority(excluded_labels: [])
+      # The order_ methods also modify the query in other ways:
+      #
+      # - For milestones, we add a JOIN.
+      # - For label priority, we change the SELECT, and add a GROUP BY.#
+      #
+      # After doing those, we need to reorder to the order we want. The existing
+      # ORDER BYs won't work because:
+      #
+      # 1. We need milestone due date first.
+      # 2. We can't ORDER BY a column that isn't in the GROUP BY and doesn't
+      #    have an aggregate function applied, so we do a useless MIN() instead.
+      #
+      milestones_due_date = 'MIN(milestones.due_date)'
+
+      order_milestone_due_asc.
+        order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]).
+        reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'),
+                Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+    end
+
+    def order_labels_priority(excluded_labels: [], extra_select_columns: [])
       params = {
         target_type: name,
         target_column: "#{table_name}.id",
@@ -162,7 +188,12 @@ module Issuable
 
       highest_priority = highest_label_priority(params).to_sql
 
-      select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+      select_columns = [
+        "#{table_name}.*",
+        "(#{highest_priority}) AS highest_priority"
+      ] + extra_select_columns
+
+      select(select_columns.join(', ')).
         group(arel_table[:id]).
         reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
     end
@@ -182,7 +213,7 @@ module Issuable
     def grouping_columns(sort)
       grouping_columns = [arel_table[:id]]
 
-      if ["milestone_due_desc", "milestone_due_asc"].include?(sort)
+      if %w(milestone_due_desc milestone_due_asc).include?(sort)
         milestone_table = Milestone.arel_table
         grouping_columns << milestone_table[:id]
         grouping_columns << milestone_table[:due_date]
@@ -232,10 +263,11 @@ module Issuable
       user: user.hook_attrs,
       project: project.hook_attrs,
       object_attributes: hook_attrs,
+      labels: labels.map(&:hook_attrs),
       # DEPRECATED
       repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
     }
-    hook_data.merge!(assignee: assignee.hook_attrs) if assignee
+    hook_data[:assignee] = assignee.hook_attrs if assignee
 
     hook_data
   end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
index e1f868a299bbb820b44092042f6500b8d23dffdb..713246039c1319ac2649ca9c4d0420d8136692bf 100644
--- a/app/models/concerns/reactive_service.rb
+++ b/app/models/concerns/reactive_service.rb
@@ -5,6 +5,6 @@ module ReactiveService
     include ReactiveCaching
 
     # Default cache key: class name + project_id
-    self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+    self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
   end
 end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1d8532a6d6f5e2dd664dbae8ecdf4555c20f735
--- /dev/null
+++ b/app/models/concerns/relative_positioning.rb
@@ -0,0 +1,139 @@
+module RelativePositioning
+  extend ActiveSupport::Concern
+
+  MIN_POSITION = 0
+  START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
+  MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+  IDEAL_DISTANCE = 500
+
+  included do
+    after_save :save_positionable_neighbours
+  end
+
+  def max_relative_position
+    self.class.in_projects(project.id).maximum(:relative_position)
+  end
+
+  def prev_relative_position
+    prev_pos = nil
+
+    if self.relative_position
+      prev_pos = self.class.
+        in_projects(project.id).
+        where('relative_position < ?', self.relative_position).
+        maximum(:relative_position)
+    end
+
+    prev_pos
+  end
+
+  def next_relative_position
+    next_pos = nil
+
+    if self.relative_position
+      next_pos = self.class.
+        in_projects(project.id).
+        where('relative_position > ?', self.relative_position).
+        minimum(:relative_position)
+    end
+
+    next_pos
+  end
+
+  def move_between(before, after)
+    return move_after(before) unless after
+    return move_before(after) unless before
+
+    # If there is no place to insert an issue we need to create one by moving the before issue closer
+    # to its predecessor. This process will recursively move all the predecessors until we have a place
+    if (after.relative_position - before.relative_position) < 2
+      before.move_before
+      @positionable_neighbours = [before]
+    end
+
+    self.relative_position = position_between(before.relative_position, after.relative_position)
+  end
+
+  def move_after(before = self)
+    pos_before = before.relative_position
+    pos_after = before.next_relative_position
+
+    if before.shift_after?
+      issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
+      issue_to_move.move_after
+      @positionable_neighbours = [issue_to_move]
+
+      pos_after = issue_to_move.relative_position
+    end
+
+    self.relative_position = position_between(pos_before, pos_after)
+  end
+
+  def move_before(after = self)
+    pos_after = after.relative_position
+    pos_before = after.prev_relative_position
+
+    if after.shift_before?
+      issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
+      issue_to_move.move_before
+      @positionable_neighbours = [issue_to_move]
+
+      pos_before = issue_to_move.relative_position
+    end
+
+    self.relative_position = position_between(pos_before, pos_after)
+  end
+
+  def move_to_end
+    self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
+  end
+
+  # Indicates if there is an issue that should be shifted to free the place
+  def shift_after?
+    next_pos = next_relative_position
+    next_pos && (next_pos - relative_position) == 1
+  end
+
+  # Indicates if there is an issue that should be shifted to free the place
+  def shift_before?
+    prev_pos = prev_relative_position
+    prev_pos && (relative_position - prev_pos) == 1
+  end
+
+  private
+
+  # This method takes two integer values (positions) and
+  # calculates the position between them. The range is huge as
+  # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
+  # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
+  def position_between(pos_before, pos_after)
+    pos_before ||= MIN_POSITION
+    pos_after ||= MAX_POSITION
+
+    pos_before, pos_after = [pos_before, pos_after].sort
+
+    halfway = (pos_after + pos_before) / 2
+    distance_to_halfway = pos_after - halfway
+
+    if distance_to_halfway < IDEAL_DISTANCE
+      halfway
+    else
+      if pos_before == MIN_POSITION
+        pos_after - IDEAL_DISTANCE
+      elsif pos_after == MAX_POSITION
+        pos_before + IDEAL_DISTANCE
+      else
+        halfway
+      end
+    end
+  end
+
+  def save_positionable_neighbours
+    return unless @positionable_neighbours
+
+    status = @positionable_neighbours.all?(&:save)
+    @positionable_neighbours = nil
+
+    status
+  end
+end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3a3c4b3d7d7f37b9c19d9ed5cfd494..529fb5ce988be46d07cda9ff5e6d7fbb4d9e9da3 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
 
       paths.each do |path|
         path = connection.quote(path)
-        where = "(routes.path = #{path})"
 
-        if cast_lower
-          where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
-        end
+        where =
+          if cast_lower
+            "(LOWER(routes.path) = LOWER(#{path}))"
+          else
+            "(routes.path = #{path})"
+          end
 
         wheres << where
       end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7edb0acd56c75d9f48187e3aa6ee2339b835c3a3..b9a2d812edd429e9d3b3d4af0d2eacc2c0812921 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -46,11 +46,12 @@ module Sortable
         where("label_links.target_id = #{target_column}").
         reorder(nil)
 
-      if target_type_column
-        query = query.where("label_links.target_type = #{target_type_column}")
-      else
-        query = query.where(label_links: { target_type: target_type })
-      end
+      query =
+        if target_type_column
+          query.where("label_links.target_type = #{target_type_column}")
+        else
+          query.where(label_links: { target_type: target_type })
+        end
 
       query = query.where.not(title: excluded_labels) if excluded_labels.present?
 
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7fe5951b6e93c6ee91ba415387cdfc990c345b9
--- /dev/null
+++ b/app/models/concerns/uniquify.rb
@@ -0,0 +1,30 @@
+class Uniquify
+  # Return a version of the given 'base' string that is unique
+  # by appending a counter to it. Uniqueness is determined by
+  # repeated calls to the passed block.
+  #
+  # If `base` is a function/proc, we expect that calling it with a
+  # candidate counter returns a string to test/return.
+  def string(base)
+    @base = base
+    @counter = nil
+
+    increment_counter! while yield(base_string)
+    base_string
+  end
+
+  private
+
+  def base_string
+    if @base.respond_to?(:call)
+      @base.call(@counter)
+    else
+      "#{@base}#{@counter}"
+    end
+  end
+
+  def increment_counter!
+    @counter ||= 0
+    @counter += 1
+  end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 559b30759050c58305cedbba8ace0df22e78fa51..895a91139c98a1e69d8c4e03069ffba3db8c6a3a 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -8,7 +8,7 @@ class DiffNote < Note
   validates :position, presence: true
   validates :diff_line, presence: true
   validates :line_code, presence: true, line_code: true
-  validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+  validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) }
   validates :resolved_by, presence: true, if: :resolved?
   validate :positions_complete
   validate :verify_supported
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 1a21b5e52b5435c826027eb022aa9e8992510d50..bf33010fd21f270209b948e3511422e53609ef15 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base
     project.deployment_service.terminals(self) if has_terminals?
   end
 
+  def has_metrics?
+    project.monitoring_service.present? && available? && last_deployment.present?
+  end
+
+  def metrics
+    project.monitoring_service.metrics(self) if has_metrics?
+  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:
diff --git a/app/models/event.rb b/app/models/event.rb
index e5027df3f8a63cb922d4aeeb8f8054c4c0d50f08..5c34844b5d30a86573bf11ab50ff30775aeff2b1 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
 
   RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
 
-  delegate :name, :email, to: :author, prefix: true, allow_nil: true
+  delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
   delegate :title, to: :issue, prefix: true, allow_nil: true
   delegate :title, to: :merge_request, prefix: true, allow_nil: true
   delegate :title, to: :note, prefix: true, allow_nil: true
@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
   scope :code_push, -> { where(action: PUSHED) }
 
   scope :in_projects, ->(projects) do
-    where(project_id: projects).recent
+    where(project_id: projects.pluck(:id)).recent
   end
 
   scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
     def contributions
       where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
             Event::PUSHED,
-            ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED],
+            %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
             "Note", Event::COMMENTED)
     end
 
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 26712c19b5a3b35c9935a3032ce2e5d601d246a7..e63f89a9f851abf369a345d5b15b500f9a1a6926 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -24,6 +24,11 @@ class ExternalIssue
   def ==(other)
     other.is_a?(self.class) && (to_s == other.to_s)
   end
+  alias_method :eql?, :==
+
+  def hash
+    [self.class, to_s].hash
+  end
 
   def project
     @project
@@ -43,7 +48,7 @@ class ExternalIssue
   end
 
   def reference_link_text(from_project = nil)
-    return "##{id}" if /^\d+$/.match(id)
+    return "##{id}" if id =~ /^\d+$/
 
     id
   end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index b991d78e27fc22bbd503300c83c52e39b4ed02f1..0afbca2cb325edd0fc2b9fb3e6510d5e88ccc70e 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -28,6 +28,28 @@ class GlobalMilestone
     new(title, child_milestones)
   end
 
+  def self.states_count(projects)
+    relation = MilestonesFinder.new.execute(projects, state: 'all')
+    milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
+
+    opened = count_by_state(milestones_by_state_and_title, 'active')
+    closed = count_by_state(milestones_by_state_and_title, 'closed')
+    all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
+
+    { 
+      opened: opened,
+      closed: closed,
+      all: all
+    }
+  end
+
+  def self.count_by_state(milestones_by_state_and_title, state)
+    milestones_by_state_and_title.count do |(milestone_state, _), _|
+      milestone_state == state
+    end
+  end
+  private_class_method :count_by_state
+
   def initialize(title, milestones)
     @title = title
     @name = title
diff --git a/app/models/group.rb b/app/models/group.rb
index 240a17f1dc181bb693cb432cc93bc6a3fcfb4d89..bd0ecae3da49791ee61d41e4086ba77df426ecff 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -28,6 +28,7 @@ class Group < Namespace
   validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
 
   mount_uploader :avatar, AvatarUploader
+  has_many :uploads, as: :model, dependent: :destroy
 
   after_create :post_create_hook
   after_destroy :post_destroy_hook
@@ -93,7 +94,7 @@ class Group < Namespace
   end
 
   def visibility_level_field
-    visibility_level
+    :visibility_level
   end
 
   def visibility_level_allowed_by_projects
@@ -212,4 +213,14 @@ class Group < Namespace
   def users_with_parents
     User.where(id: members_with_parents.select(:user_id))
   end
+
+  def mattermost_team_params
+    max_length = 59
+
+    {
+      name: path[0..max_length],
+      display_name: name[0..max_length],
+      type: public? ? 'O' : 'I' # Open vs Invite-only
+    }
+  end
 end
diff --git a/app/models/guest.rb b/app/models/guest.rb
index 01285ca12644d6d044f46f19853fc4901e775c5b..df287c277a79a6ee7a38ecf054e1dc62890f10d5 100644
--- a/app/models/guest.rb
+++ b/app/models/guest.rb
@@ -1,6 +1,6 @@
 class Guest
   class << self
-    def can?(action, subject)
+    def can?(action, subject = :global)
       Ability.allowed?(nil, action, subject)
     end
   end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d8826b65fcc2edd7ca9ef99bc82ac01009fd4392..602eed86d9ed78035fc10933dc6a93bdb3af764b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
   include Sortable
   include Spammable
   include FasterCacheKeys
+  include RelativePositioning
 
   DueDateStruct = Struct.new(:title, :name).freeze
   NoDueDate     = DueDateStruct.new('No Due Date', '0').freeze
@@ -15,8 +16,6 @@ class Issue < ActiveRecord::Base
   DueThisWeek   = DueDateStruct.new('Due This Week', 'week').freeze
   DueThisMonth  = DueDateStruct.new('Due This Month', 'month').freeze
 
-  ActsAsTaggableOn.strict_case_match = true
-
   belongs_to :project
   belongs_to :moved_to, class_name: 'Issue'
 
@@ -56,10 +55,24 @@ class Issue < ActiveRecord::Base
     state :opened
     state :reopened
     state :closed
+
+    before_transition any => :closed do |issue|
+      issue.closed_at = Time.zone.now
+    end
+
+    before_transition closed: any do |issue|
+      issue.closed_at = nil
+    end
   end
 
   def hook_attrs
-    attributes
+    attrs = {
+      total_time_spent: total_time_spent,
+      human_total_time_spent: human_total_time_spent,
+      human_time_estimate: human_time_estimate
+    }
+
+    attributes.merge!(attrs)
   end
 
   def self.reference_prefix
@@ -97,6 +110,13 @@ class Issue < ActiveRecord::Base
     end
   end
 
+  def self.order_by_position_and_priority
+    order_labels_priority.
+      reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
+              Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
+              "id DESC")
+  end
+
   # `from` argument can be a Namespace or Project.
   def to_reference(from = nil, full: false)
     reference = "#{self.class.reference_prefix}#{iid}"
diff --git a/app/models/label.rb b/app/models/label.rb
index 5b6b9a7a736798ca84c9fe27a02dbf8fa95adcb8..568fa6d44f520fd80c9c4e464a577681b873212c 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -11,7 +11,7 @@ class Label < ActiveRecord::Base
 
   cache_markdown_field :description, pipeline: :single_line
 
-  DEFAULT_COLOR = '#428BCA'
+  DEFAULT_COLOR = '#428BCA'.freeze
 
   default_value_for :color, DEFAULT_COLOR
 
@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base
     end
   end
 
+  def hook_attrs
+    attributes
+  end
+
   private
 
   def issues_count(user, params = {})
diff --git a/app/models/member.rb b/app/models/member.rb
index d07f270b7570a19b3a11e6e2fe3813bbda584a49..0545bd4eedfc5a44590484172539f34c49b1bb02 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -10,6 +10,8 @@ class Member < ActiveRecord::Base
   belongs_to :user
   belongs_to :source, polymorphic: true
 
+  delegate :name, :username, :email, to: :user, prefix: true
+
   validates :user, presence: true, unless: :invite?
   validates :source, presence: true
   validates :user_id, uniqueness: { scope: [:source_type, :source_id],
@@ -73,8 +75,6 @@ class Member < ActiveRecord::Base
   after_destroy :post_destroy_hook, unless: :pending?
   after_commit :refresh_member_authorized_projects
 
-  delegate :name, :username, :email, to: :user, prefix: true
-
   default_value_for :notification_level, NotificationSetting.levels[:global]
 
   class << self
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 204f34f026944a967978159e98e7caf0a60e8cd5..446f9f8f8a7288e141d7a14a9eafa7b7867f876b 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,11 +1,11 @@
 class GroupMember < Member
-  SOURCE_TYPE = 'Namespace'
+  SOURCE_TYPE = 'Namespace'.freeze
 
   belongs_to :group, foreign_key: 'source_id'
 
   # Make sure group member points only to group as it source
   default_value_for :source_type, SOURCE_TYPE
-  validates_format_of :source_type, with: /\ANamespace\z/
+  validates :source_type, format: { with: /\ANamespace\z/ }
   default_scope { where(source_type: SOURCE_TYPE) }
 
   def self.access_level_roles
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 008fff0857c35af5748033ba9b4b0347c1321516..912820b51ac32218a7c5722cf7c25dcff0adeef5 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -1,5 +1,5 @@
 class ProjectMember < Member
-  SOURCE_TYPE = 'Project'
+  SOURCE_TYPE = 'Project'.freeze
 
   include Gitlab::ShellAdapter
 
@@ -7,7 +7,7 @@ class ProjectMember < Member
 
   # Make sure project member points only to project as it source
   default_value_for :source_type, SOURCE_TYPE
-  validates_format_of :source_type, with: /\AProject\z/
+  validates :source_type, format: { with: /\AProject\z/ }
   validates :access_level, inclusion: { in: Gitlab::Access.values }
   default_scope { where(source_type: SOURCE_TYPE) }
 
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 204d2b153ad82ca1f8e29842883809b78fb1e9c4..cef8ad76b07df9a021ab4e40f28a1559ec3b8cf1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
 
   belongs_to :target_project, class_name: "Project"
   belongs_to :source_project, class_name: "Project"
+  belongs_to :project, foreign_key: :target_project_id
   belongs_to :merge_user, class_name: "User"
 
   has_many :merge_request_diffs, dependent: :destroy
@@ -91,17 +92,13 @@ class MergeRequest < ActiveRecord::Base
     around_transition do |merge_request, transition, block|
       Gitlab::Timeless.timeless(merge_request, &block)
     end
-
-    after_transition unchecked: :cannot_be_merged do |merge_request, transition|
-      TodoService.new.merge_request_became_unmergeable(merge_request)
-    end
   end
 
   validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
   validates :source_branch, presence: true
   validates :target_project, presence: true
   validates :target_branch, presence: true
-  validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing?
+  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
   validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
   validate :validate_fork, unless: :closed_without_fork?
 
@@ -203,7 +200,11 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def diff_size
-    opts = diff_options || {}
+    # The `#diffs` method ends up at an instance of a class inheriting from
+    # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
+    # here too, to get the same diff size without performing highlighting.
+    #
+    opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
 
     raw_diffs(opts).size
   end
@@ -436,7 +437,7 @@ class MergeRequest < ActiveRecord::Base
     true
   end
 
-  def can_cancel_merge_when_build_succeeds?(current_user)
+  def can_cancel_merge_when_pipeline_succeeds?(current_user)
     can_be_merged_by?(current_user) || self.author == current_user
   end
 
@@ -523,11 +524,14 @@ class MergeRequest < ActiveRecord::Base
       source: source_project.try(:hook_attrs),
       target: target_project.hook_attrs,
       last_commit: nil,
-      work_in_progress: work_in_progress?
+      work_in_progress: work_in_progress?,
+      total_time_spent: total_time_spent,
+      human_total_time_spent: human_total_time_spent,
+      human_time_estimate: human_time_estimate
     }
 
     if diff_head_commit
-      attrs.merge!(last_commit: diff_head_commit.hook_attrs)
+      attrs[:last_commit] = diff_head_commit.hook_attrs
     end
 
     attributes.merge!(attrs)
@@ -537,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
     target_project != source_project
   end
 
-  def project
-    target_project
-  end
-
   # If the merge request closes any issues, save this information in the
   # `MergeRequestsClosingIssues` model. This is a performance optimization.
   # Calculating this information for a number of merge requests requires
@@ -644,10 +644,10 @@ class MergeRequest < ActiveRecord::Base
     message.join("\n\n")
   end
 
-  def reset_merge_when_build_succeeds
-    return unless merge_when_build_succeeds?
+  def reset_merge_when_pipeline_succeeds
+    return unless merge_when_pipeline_succeeds?
 
-    self.merge_when_build_succeeds = false
+    self.merge_when_pipeline_succeeds = false
     self.merge_user = nil
     if merge_params
       merge_params.delete('should_remove_source_branch')
@@ -684,7 +684,10 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def has_ci?
-    source_project.try(:ci_service) && commits.any?
+    has_ci_integration = source_project.try(:ci_service)
+    uses_gitlab_ci = all_pipelines.any?
+
+    (has_ci_integration || uses_gitlab_ci) && commits.any?
   end
 
   def branch_missing?
@@ -706,7 +709,7 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def mergeable_ci_state?
-    return true unless project.only_allow_merge_if_build_succeeds?
+    return true unless project.only_allow_merge_if_pipeline_succeeds?
 
     !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
   end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 70bad2a4396aecf039fd42e5ca68393d4e3ba228..baee00b8fcde5a286ddaaabfa0ce246faaaf8604 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base
   COMMITS_SAFE_SIZE = 100
 
   # Valid types of serialized diffs allowed by Gitlab::Git::Diff
-  VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+  VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
 
   belongs_to :merge_request
 
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
index ab597c379471afa8becb49d0bcfaaa78137b8239..daafb137be4c2be3bb7eb360abad55d025ac10df 100644
--- a/app/models/merge_requests_closing_issues.rb
+++ b/app/models/merge_requests_closing_issues.rb
@@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base
 
   validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
   validates :issue_id, presence: true
+
+  class << self
+    def count_for_collection(ids)
+      group(:issue_id).
+        where(issue_id: ids).
+        pluck('issue_id', 'COUNT(*) as count')
+    end
+  end
 end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 7331000a9f2de4bd43ebc6f2dd2cf573eb54c40d..c0deb59ec4c9ba5cd56e7eb91c763c2a522bd8d7 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base
   None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
   Any = MilestoneStruct.new('Any Milestone', '', -1)
   Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+  Started = MilestoneStruct.new('Started', '#started', -3)
 
   include CacheMarkdownField
   include InternalId
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a803be2e780223f3049d82394318670b8a7a9c2f..4ae9d0122f287a72f453550ecdb38eb85141d3a7 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -20,6 +20,7 @@ class Namespace < ActiveRecord::Base
 
   belongs_to :parent, class_name: "Namespace"
   has_many :children, class_name: "Namespace", foreign_key: :parent_id
+  has_one :chat_team, dependent: :destroy
 
   validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
   validates :name,
@@ -98,14 +99,8 @@ class Namespace < ActiveRecord::Base
       # Work around that by setting their username to "blank", followed by a counter.
       path = "blank" if path.blank?
 
-      counter = 0
-      base = path
-      while Namespace.find_by_path_or_name(path)
-        counter += 1
-        path = "#{base}#{counter}"
-      end
-
-      path
+      uniquify = Uniquify.new
+      uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
     end
   end
 
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index b524ca50ee820e041153a570700dd35436b4b308..0bbc9451ffdb466d24dbf10fc58a39b3c24dcdf5 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -188,11 +188,12 @@ module Network
       end
 
       # and mark it as reserved
-      if parent_time.nil?
-        min_time = leaves.first.time
-      else
-        min_time = parent_time + 1
-      end
+      min_time =
+        if parent_time.nil?
+          leaves.first.time
+        else
+          parent_time + 1
+        end
 
       max_time = leaves.last.time
       leaves.last.parents(@map).each do |parent|
diff --git a/app/models/note.rb b/app/models/note.rb
index 029fe667a45d29f719762bfa0fc3bb5462e14214..e22e96aec6fe869a42c4a035b2c53b782071be3c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -72,7 +72,7 @@ class Note < ActiveRecord::Base
   scope :inc_author, ->{ includes(:author) }
   scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
 
-  scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
+  scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
   scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
 
   scope :with_associations, -> do
@@ -85,6 +85,7 @@ class Note < ActiveRecord::Base
   before_validation :nullify_blank_type, :nullify_blank_line_code
   before_validation :set_discussion_id
   after_save :keep_around_commit, unless: :for_personal_snippet?
+  after_save :expire_etag_cache
 
   class << self
     def model_name
@@ -231,10 +232,6 @@ class Note < ActiveRecord::Base
     note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
   end
 
-  def award_emoji_name
-    note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
-  end
-
   def to_ability_name
     for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
   end
@@ -276,4 +273,16 @@ class Note < ActiveRecord::Base
       self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
     end
   end
+
+  def expire_etag_cache
+    return unless for_issue?
+
+    key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path(
+      noteable.project.namespace,
+      noteable.project,
+      target_type: noteable_type.underscore,
+      target_id: noteable.id
+    )
+    Gitlab::EtagCaching::Store.new.touch(key)
+  end
 end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 58f6214bea77ed2209b030de23d4aae767983a20..52577bd52ea3043c1dd40b9a0578dac72223757b 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -35,11 +35,11 @@ class NotificationSetting < ActiveRecord::Base
     :merge_merge_request,
     :failed_pipeline,
     :success_pipeline
-  ]
+  ].freeze
 
   EXCLUDED_WATCHER_EVENTS = [
     :success_pipeline
-  ]
+  ].freeze
 
   store :events, accessors: EMAIL_EVENTS, coder: JSON
 
diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a997406565d490314bb2053aaafdbe6ff3d5e25
--- /dev/null
+++ b/app/models/oauth_access_grant.rb
@@ -0,0 +1,4 @@
+class OauthAccessGrant < Doorkeeper::AccessGrant
+  belongs_to :resource_owner, class_name: 'User'
+  belongs_to :application, class_name: 'Doorkeeper::Application'
+end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 116fb71ac083e20467d6ea4d058eb26ad68eee53..b85f5dbaf2e3a6d972c3548658b3165763452d40 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -1,4 +1,4 @@
-class OauthAccessToken < ActiveRecord::Base
+class OauthAccessToken < Doorkeeper::AccessToken
   belongs_to :resource_owner, class_name: 'User'
   belongs_to :application, class_name: 'Doorkeeper::Application'
 end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 0b9ebf1ffe20f30c6497369d2799a94257e141d3..f2f2fc1e32a3655320c3efa7dda035d6aaea2c2c 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -2,7 +2,7 @@ class PagesDomain < ActiveRecord::Base
   belongs_to :project
 
   validates :domain, hostname: true
-  validates_uniqueness_of :domain, case_sensitive: false
+  validates :domain, uniqueness: { case_sensitive: false }
   validates :certificate, certificate: true, allow_nil: true, allow_blank: true
   validates :key, certificate_key: true, allow_nil: true, allow_blank: true
 
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 10a34c42fd8e828c5fb8c9c07d40e889b2c95c20..e8b000ddad6247f05dbf31885fee025eadfffaf2 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -1,4 +1,5 @@
 class PersonalAccessToken < ActiveRecord::Base
+  include Expirable
   include TokenAuthenticatable
   add_authentication_token_field :token
 
@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base
 
   belongs_to :user
 
-  scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+  before_save :ensure_token
+
+  scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
   scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+  scope :with_impersonation, -> { where(impersonation: true) }
+  scope :without_impersonation, -> { where(impersonation: false) }
 
-  def self.generate(params)
-    personal_access_token = self.new(params)
-    personal_access_token.ensure_token
-    personal_access_token
-  end
+  validates :scopes, presence: true
+  validate :validate_api_scopes
 
   def revoke!
     self.revoked = true
     self.save
   end
+
+  def active?
+    !revoked? && !expired?
+  end
+
+  protected
+
+  def validate_api_scopes
+    unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
+      errors.add :scopes, "can only contain API scopes"
+    end
+  end
 end
diff --git a/app/models/project.rb b/app/models/project.rb
index d4f5584f53d3560fa2ae02ed0dee077837e8e046..928965643a04598ae8568dd1dc4b1304b1f90c74 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,10 +19,10 @@ class Project < ActiveRecord::Base
 
   extend Gitlab::ConfigHelper
 
-  class BoardLimitExceeded < StandardError; end
+  BoardLimitExceeded = Class.new(StandardError)
 
   NUMBER_OF_PERMITTED_BOARDS = 1
-  UNKNOWN_IMPORT_URL = 'http://unknown.git'
+  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
 
   cache_markdown_field :description, pipeline: :description
 
@@ -70,8 +70,7 @@ class Project < ActiveRecord::Base
 
   after_validation :check_pending_delete
 
-  ActsAsTaggableOn.strict_case_match = true
-  acts_as_taggable_on :tags
+  acts_as_taggable
 
   attr_accessor :new_default_branch
   attr_accessor :old_path_with_namespace
@@ -90,7 +89,6 @@ class Project < ActiveRecord::Base
   has_one :campfire_service, dependent: :destroy
   has_one :drone_ci_service, dependent: :destroy
   has_one :emails_on_push_service, dependent: :destroy
-  has_one :builds_email_service, dependent: :destroy
   has_one :pipelines_email_service, dependent: :destroy
   has_one :irker_service, dependent: :destroy
   has_one :pivotaltracker_service, dependent: :destroy
@@ -114,6 +112,8 @@ class Project < ActiveRecord::Base
   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 :prometheus_service, dependent: :destroy, inverse_of: :project
+  has_one :mock_ci_service, dependent: :destroy
 
   has_one  :forked_project_link,  dependent: :destroy, foreign_key: "forked_to_project_id"
   has_one  :forked_from_project,  through:   :forked_project_link
@@ -159,13 +159,13 @@ class Project < ActiveRecord::Base
   has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
   has_many :container_images, dependent: :destroy
 
-  has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
-  has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
-  has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
-  has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+  has_many :commit_statuses, dependent: :destroy
+  has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+  has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+  has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
   has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
-  has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
-  has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+  has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+  has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
   has_many :environments, dependent: :destroy
   has_many :deployments, dependent: :destroy
 
@@ -173,9 +173,11 @@ class Project < ActiveRecord::Base
   accepts_nested_attributes_for :project_feature
 
   delegate :name, to: :owner, allow_nil: true, prefix: true
+  delegate :count, to: :forks, prefix: true
   delegate :members, to: :team, prefix: true
   delegate :add_user, to: :team
   delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
+  delegate :empty_repo?, to: :repository
 
   # Validations
   validates :creator, presence: true, on: :create
@@ -192,9 +194,10 @@ class Project < ActiveRecord::Base
     format: { with: Gitlab::Regex.project_path_regex,
               message: Gitlab::Regex.project_path_regex_message }
   validates :namespace, presence: true
-  validates_uniqueness_of :name, scope: :namespace_id
-  validates_uniqueness_of :path, scope: :namespace_id
+  validates :name, uniqueness: { scope: :namespace_id }
+  validates :path, uniqueness: { scope: :namespace_id }
   validates :import_url, addressable_url: true, if: :external_import?
+  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
   validates :star_count, numericality: { greater_than_or_equal_to: 0 }
   validate :check_limit, on: :create
   validate :avatar_type,
@@ -211,6 +214,7 @@ class Project < ActiveRecord::Base
   before_save :ensure_runners_token
 
   mount_uploader :avatar, AvatarUploader
+  has_many :uploads, as: :model, dependent: :destroy
 
   # Scopes
   default_scope { where(pending_delete: false) }
@@ -334,7 +338,7 @@ class Project < ActiveRecord::Base
     end
 
     def search_by_visibility(level)
-      where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase))
+      where(visibility_level: Gitlab::VisibilityLevel.string_options[level])
     end
 
     def search_by_title(query)
@@ -359,7 +363,7 @@ class Project < ActiveRecord::Base
     end
 
     def reference_pattern
-      name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
+      name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
 
       %r{
         ((?<namespace>#{name_pattern})\/)?
@@ -390,7 +394,7 @@ class Project < ActiveRecord::Base
   end
 
   def repository_storage_path
-    Gitlab.config.repositories.storages[repository_storage]
+    Gitlab.config.repositories.storages[repository_storage]['path']
   end
 
   def team
@@ -452,13 +456,14 @@ class Project < ActiveRecord::Base
   end
 
   def add_import_job
-    if forked?
-      job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
-                                                  forked_from_project.path_with_namespace,
-                                                  self.namespace.full_path)
-    else
-      job_id = RepositoryImportWorker.perform_async(self.id)
-    end
+    job_id =
+      if forked?
+        RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
+          forked_from_project.path_with_namespace,
+          self.namespace.full_path)
+      else
+        RepositoryImportWorker.perform_async(self.id)
+      end
 
     if job_id
       Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
@@ -766,6 +771,14 @@ class Project < ActiveRecord::Base
     @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
   end
 
+  def monitoring_services
+    services.where(category: :monitoring)
+  end
+
+  def monitoring_service
+    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
+  end
+
   def jira_tracker?
     issues_tracker.to_param == 'jira'
   end
@@ -836,10 +849,6 @@ class Project < ActiveRecord::Base
     false
   end
 
-  def empty_repo?
-    repository.empty_repo?
-  end
-
   def repo
     repository.raw
   end
@@ -848,10 +857,6 @@ class Project < ActiveRecord::Base
     gitlab_shell.url_to_repo(path_with_namespace)
   end
 
-  def namespace_dir
-    namespace.try(:path) || ''
-  end
-
   def repo_exists?
     @repo_exists ||= repository.exists?
   rescue
@@ -874,8 +879,10 @@ class Project < ActiveRecord::Base
     url_to_repo
   end
 
-  def http_url_to_repo
-    "#{web_url}.git"
+  def http_url_to_repo(user = nil)
+    credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+    Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
   end
 
   # Check if current branch name is marked as protected in the system
@@ -900,8 +907,8 @@ class Project < ActiveRecord::Base
 
   def rename_repo
     path_was = previous_changes['path'].first
-    old_path_with_namespace = File.join(namespace_dir, path_was)
-    new_path_with_namespace = File.join(namespace_dir, path)
+    old_path_with_namespace = File.join(namespace.full_path, path_was)
+    new_path_with_namespace = File.join(namespace.full_path, path)
 
     Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
 
@@ -1002,7 +1009,7 @@ class Project < ActiveRecord::Base
   end
 
   def visibility_level_field
-    visibility_level
+    :visibility_level
   end
 
   def archive!
@@ -1027,10 +1034,6 @@ class Project < ActiveRecord::Base
     forked? && project == forked_from_project
   end
 
-  def forks_count
-    forks.count
-  end
-
   def origin_merge_requests
     merge_requests.where(source_project_id: self.id)
   end
@@ -1201,6 +1204,10 @@ class Project < ActiveRecord::Base
     end
   end
 
+  def pipeline_status
+    @pipeline_status ||= Ci::PipelineStatus.load_for_project(self)
+  end
+
   def mark_import_as_failed(error_message)
     original_errors = errors.dup
     sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 03194fc2141356fd731c71c89a036af7cd575ccd..e3ef4919b2877ec7caa094a836289c50aa6742b2 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -18,7 +18,7 @@ class ProjectFeature < ActiveRecord::Base
   PRIVATE  = 10
   ENABLED  = 20
 
-  FEATURES = %i(issues merge_requests wiki snippets builds repository)
+  FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
 
   class << self
     def access_level_attribute(feature)
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 5cb6b0c527d1505b1e7dd7cab47f8f463e92ffcd..ac1e9ab2b0be6f1e347dbfbbfbbfca54be185fb6 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base
   private
 
   def different_group
-    if self.group && self.project && self.project.group == self.group
-      errors.add(:base, "Project cannot be shared with the project it is in.")
+    return unless self.group && self.project
+
+    project_group = self.project.group
+    return unless project_group
+
+    group_ids = project_group.ancestors.map(&:id).push(project_group.id)
+
+    if group_ids.include?(self.group.id)
+      errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.")
     end
   end
 
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 0956c4a4ede143a41930732633962ee2505941c6..5fb95050b83847aa674230eb9b44a01d6440b097 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,7 @@ require "addressable/uri"
 class BuildkiteService < CiService
   include ReactiveService
 
-  ENDPOINT = "https://buildkite.com"
+  ENDPOINT = "https://buildkite.com".freeze
 
   prop_accessor :project_url, :token
   boolean_accessor :enable_ssl_verification
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e3718910bcb25076fbff710d7b97e93c4d3..0c526b53d724c1012f6cba22062743904a0e6f72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
 class BuildsEmailService < Service
-  prop_accessor :recipients
-  boolean_accessor :add_pusher
-  boolean_accessor :notify_only_broken_builds
-  validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
-  def initialize_properties
-    if properties.nil?
-      self.properties = {}
-      self.notify_only_broken_builds = true
-    end
-  end
-
-  def title
-    'Builds emails'
-  end
-
-  def description
-    'Email the builds status to a list of recipients.'
-  end
-
   def self.to_param
     'builds_email'
   end
 
   def self.supported_events
-    %w(build)
-  end
-
-  def execute(push_data)
-    return unless supported_events.include?(push_data[:object_kind])
-    return unless should_build_be_notified?(push_data)
-
-    recipients = all_recipients(push_data)
-
-    if recipients.any?
-      BuildEmailWorker.perform_async(
-        push_data[:build_id],
-        recipients,
-        push_data
-      )
-    end
-  end
-
-  def can_test?
-    project.builds.any?
-  end
-
-  def disabled_title
-    "Please setup a build on your repository."
-  end
-
-  def test_data(project = nil, user = nil)
-    Gitlab::DataBuilder::Build.build(project.builds.last)
-  end
-
-  def fields
-    [
-      { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
-      { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
-    ]
-  end
-
-  def test(data)
-    begin
-      # bypass build status verification when testing
-      data[:build_status] = "failed"
-      data[:build_allow_failure] = false
-
-      result = execute(data)
-    rescue StandardError => error
-      return { success: false, result: error }
-    end
-
-    { success: true, result: result }
-  end
-
-  def should_build_be_notified?(data)
-    case data[:build_status]
-    when 'success'
-      !notify_only_broken_builds?
-    when 'failed'
-      !allow_failure?(data)
-    else
-      false
-    end
-  end
-
-  def allow_failure?(data)
-    data[:build_allow_failure] == true
-  end
-
-  def all_recipients(data)
-    all_recipients = []
-
-    unless recipients.blank?
-      all_recipients += recipients.split(',').compact.reject(&:blank?)
-    end
-
-    if add_pusher? && data[:user][:email]
-      all_recipients << data[:user][:email]
-    end
-
-    all_recipients
+    %w[]
   end
 end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c411fb964786fddf00390d6e08cc4d9..0000000000000000000000000000000000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
-  class BuildMessage < BaseMessage
-    attr_reader :sha
-    attr_reader :ref_type
-    attr_reader :ref
-    attr_reader :status
-    attr_reader :project_name
-    attr_reader :project_url
-    attr_reader :user_name
-    attr_reader :user_url
-    attr_reader :duration
-    attr_reader :stage
-    attr_reader :build_id
-    attr_reader :build_name
-
-    def initialize(params)
-      @sha = params[:sha]
-      @ref_type = params[:tag] ? 'tag' : 'branch'
-      @ref = params[:ref]
-      @project_name = params[:project_name]
-      @project_url = params[:project_url]
-      @status = params[:commit][:status]
-      @user_name = params[:commit][:author_name]
-      @user_url = params[:commit][:author_url]
-      @duration = params[:commit][:duration]
-      @stage = params[:build_stage]
-      @build_name = params[:build_name]
-      @build_id = params[:build_id]
-    end
-
-    def pretext
-      ''
-    end
-
-    def fallback
-      format(message)
-    end
-
-    def attachments
-      [{ text: format(message), color: attachment_color }]
-    end
-
-    private
-
-    def message
-      "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
-    end
-
-    def build_url
-      "#{project_url}/builds/#{build_id}"
-    end
-
-    def build_link
-      link(build_name, build_url)
-    end
-
-    def user_link
-      link(user_name, user_url)
-    end
-
-    def format(string)
-      Slack::Notifier::LinkFormatter.format(string)
-    end
-
-    def humanized_status
-      case status
-      when 'success'
-        'passed'
-      else
-        status
-      end
-    end
-
-    def attachment_color
-      if status == 'success'
-        'good'
-      else
-        'danger'
-      end
-    end
-
-    def branch_url
-      "#{project_url}/commits/#{ref}"
-    end
-
-    def branch_link
-      link(ref, branch_url)
-    end
-
-    def project_link
-      link(project_name, project_url)
-    end
-
-    def commit_url
-      "#{project_url}/commit/#{sha}/builds"
-    end
-
-    def commit_link
-      link(Commit.truncate_sha(sha), commit_url)
-    end
-  end
-end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index b96aca47e65969d00ecae36347ffc5553b900a22..791e5b0cec79396588b9edca51e17f9aac1c0733 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -51,7 +51,8 @@ module ChatMessage
         title: issue_title,
         title_link: issue_url,
         text: format(description),
-        color: "#C95823" }]
+        color: "#C95823"
+      }]
     end
 
     def project_link
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425fc113a0ee40147528820d9165c13a0..200be99f36b8e42da5c28cc1147b1fbbcece5218 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
   default_value_for :category, 'chat'
 
   prop_accessor :webhook, :username, :channel
-  boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+  boolean_accessor :notify_only_broken_pipelines
 
   validates :webhook, presence: true, url: true, if: :activated?
 
@@ -16,7 +16,6 @@ class ChatNotificationService < Service
 
     if properties.nil?
       self.properties = {}
-      self.notify_only_broken_builds = true
       self.notify_only_broken_pipelines = true
     end
   end
@@ -27,7 +26,7 @@ class ChatNotificationService < Service
 
   def self.supported_events
     %w[push issue confidential_issue merge_request note tag_push
-       build pipeline wiki_page]
+       pipeline wiki_page]
   end
 
   def execute(data)
@@ -89,8 +88,6 @@ class ChatNotificationService < Service
       ChatMessage::MergeMessage.new(data) unless is_update?(data)
     when "note"
       ChatMessage::NoteMessage.new(data)
-    when "build"
-      ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
     when "pipeline"
       ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
     when "wiki_page"
@@ -125,17 +122,6 @@ class ChatNotificationService < Service
     data[:object_attributes][:action] == 'update'
   end
 
-  def should_build_be_notified?(data)
-    case data[:commit][:status]
-    when 'success'
-      !notify_only_broken_builds?
-    when 'failed'
-      true
-    else
-      false
-    end
-  end
-
   def should_pipeline_be_notified?(data)
     case data[:object_attributes][:status]
     when 'success'
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 1ad9efac1964e0ac4988fb7d7bed8d6a210a8565..2717c240f05192034f3e43bd2d9b0aa4bc1a037e 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -39,7 +39,7 @@ class DroneCiService < CiService
   def commit_status_path(sha, ref)
     url = [drone_url,
            "gitlab/#{project.full_path}/commits/#{sha}",
-           "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
+           "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"]
 
     URI.join(*url).to_s
   end
@@ -74,7 +74,7 @@ class DroneCiService < CiService
   def build_page(sha, ref)
     url = [drone_url,
            "gitlab/#{project.full_path}/redirect/commits/#{sha}",
-           "?branch=#{URI::encode(ref.to_s)}"]
+           "?branch=#{URI.encode(ref.to_s)}"]
 
     URI.join(*url).to_s
   end
@@ -114,7 +114,7 @@ class DroneCiService < CiService
   end
 
   def merge_request_valid?(data)
-    ['opened', 'reopened'].include?(data[:object_attributes][:state]) &&
+    %w(opened reopened).include?(data[:object_attributes][:state]) &&
       data[:object_attributes][:merge_status] == 'unchecked'
   end
 end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 72da219df28bf2ca8aea5569f02c5617f96dd029..8b181221bb044cf9689085dcd0888ccf9954a5b8 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -6,16 +6,16 @@ class HipchatService < Service
     a b i strong em br img pre code
     table th tr td caption colgroup col thead tbody tfoot
     ul ol li dl dt dd
-  ]
+  ].freeze
 
   prop_accessor :token, :room, :server, :color, :api_version
-  boolean_accessor :notify_only_broken_builds, :notify
+  boolean_accessor :notify_only_broken_pipelines, :notify
   validates :token, presence: true, if: :activated?
 
   def initialize_properties
     if properties.nil?
       self.properties = {}
-      self.notify_only_broken_builds = true
+      self.notify_only_broken_pipelines = true
     end
   end
 
@@ -36,17 +36,17 @@ class HipchatService < Service
       { type: 'text', name: 'token',     placeholder: 'Room token' },
       { type: 'text', name: 'room',      placeholder: 'Room name or ID' },
       { type: 'checkbox', name: 'notify' },
-      { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] },
+      { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
       { type: 'text', name: 'api_version',
         placeholder: 'Leave blank for default (v2)' },
       { type: 'text', name: 'server',
         placeholder: 'Leave blank for default. https://hipchat.example.com' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
+      { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
 
   def self.supported_events
-    %w(push issue confidential_issue merge_request note tag_push build)
+    %w(push issue confidential_issue merge_request note tag_push pipeline)
   end
 
   def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
       create_merge_request_message(data) unless is_update?(data)
     when "note"
       create_note_message(data)
-    when "build"
-      create_build_message(data) if should_build_be_notified?(data)
+    when "pipeline"
+      create_pipeline_message(data) if should_pipeline_be_notified?(data)
     end
   end
 
@@ -240,28 +240,29 @@ class HipchatService < Service
     message
   end
 
-  def create_build_message(data)
-    ref_type = data[:tag] ? 'tag' : 'branch'
-    ref = data[:ref]
-    sha = data[:sha]
-    user_name = data[:commit][:author_name]
-    status = data[:commit][:status]
-    duration = data[:commit][:duration]
+  def create_pipeline_message(data)
+    pipeline_attributes = data[:object_attributes]
+    pipeline_id = pipeline_attributes[:id]
+    ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+    ref = pipeline_attributes[:ref]
+    user_name = (data[:user] && data[:user][:name]) || 'API'
+    status = pipeline_attributes[:status]
+    duration = pipeline_attributes[:duration]
 
     branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
-    commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+    pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
 
-    "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+    "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
   end
 
   def message_color(data)
-    build_status_color(data) || color || 'yellow'
+    pipeline_status_color(data) || color || 'yellow'
   end
 
-  def build_status_color(data)
-    return unless data && data[:object_kind] == 'build'
+  def pipeline_status_color(data)
+    return unless data && data[:object_kind] == 'pipeline'
 
-    case data[:commit][:status]
+    case data[:object_attributes][:status]
     when 'success'
       'green'
     else
@@ -294,10 +295,10 @@ class HipchatService < Service
     end
   end
 
-  def should_build_be_notified?(data)
-    case data[:commit][:status]
+  def should_pipeline_be_notified?(data)
+    case data[:object_attributes][:status]
     when 'success'
-      !notify_only_broken_builds?
+      !notify_only_broken_pipelines?
     when 'failed'
       true
     else
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 5d6862d9faa65ec2b2b177bf32e67397a77a924e..c62bb4fa120c29af99ac80c913a0234feab6faf7 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -33,7 +33,8 @@ class IrkerService < Service
   end
 
   def settings
-    { server_host: server_host.present? ? server_host : 'localhost',
+    {
+      server_host: server_host.present? ? server_host : 'localhost',
       server_port: server_port.present? ? server_port : 6659
     }
   end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 9e65fdbf9d66185e4dd918cd37f0255312694f71..50435b67eda863e8a106dc91b2ba3ad632c31695 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,4 +1,6 @@
 class IssueTrackerService < Service
+  validate :one_issue_tracker, if: :activated?, on: :manual_change
+
   default_value_for :category, 'issue_tracker'
 
   # Pattern used to extract links from comments
@@ -92,4 +94,13 @@ class IssueTrackerService < Service
   def issues_tracker
     Gitlab.config.issues_tracker[to_param]
   end
+
+  def one_issue_tracker
+    return if template?
+    return if project.blank?
+
+    if project.services.external_issue_trackers.where.not(id: id).any?
+      errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time')
+    end
+  end
 end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f2f019c43bb46f5b6a4d112098683351901fc3e8..02fbd5497fa0a081b70abb4573ad11b653be30cc 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -3,7 +3,7 @@ class KubernetesService < DeploymentService
   include Gitlab::Kubernetes
   include ReactiveCaching
 
-  self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+  self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
 
   # Namespace defaults to the project path, but can be overridden in case that
   # is an invalid or inappropriate name
@@ -36,7 +36,7 @@ class KubernetesService < DeploymentService
   def initialize_properties
     if properties.nil?
       self.properties = {}
-      self.namespace = project.path if project.present?
+      self.namespace = "#{project.path}-#{project.id}" if project.present?
     end
   end
 
@@ -62,23 +62,19 @@ class KubernetesService < DeploymentService
         { type: 'text',
           name: 'namespace',
           title: 'Kubernetes namespace',
-          placeholder: 'Kubernetes namespace',
-        },
+          placeholder: 'Kubernetes namespace' },
         { type: 'text',
           name: 'api_url',
           title: 'API URL',
-          placeholder: 'Kubernetes API URL, like https://kube.example.com/',
-        },
+          placeholder: 'Kubernetes API URL, like https://kube.example.com/' },
         { type: 'text',
           name: 'token',
           title: 'Service token',
-          placeholder: 'Service token',
-        },
+          placeholder: 'Service token' },
         { type: 'textarea',
           name: 'ca_pem',
           title: 'Custom CA bundle',
-          placeholder: 'Certificate Authority bundle (PEM format)',
-        },
+          placeholder: 'Certificate Authority bundle (PEM format)' },
     ]
   end
 
@@ -98,7 +94,12 @@ class KubernetesService < DeploymentService
       { 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?
+
+    if ca_pem.present?
+      variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
+      variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+    end
+
     variables
   end
 
@@ -167,7 +168,7 @@ class KubernetesService < DeploymentService
     url = URI.parse(api_url)
     prefix = url.path.sub(%r{/+\z}, '')
 
-    url.path = [ prefix, *parts ].join("/")
+    url.path = [prefix, *parts].join("/")
 
     url.to_s
   end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index 4ebc5318da1f36c14a4bdedf1b566cfa2530d608..1156d05062203cfe2cff7fea2e8bb121594e455b 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
     '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>
+      <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 below.</li>
+      <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
     </ol>'
   end
 
@@ -28,14 +28,13 @@ class MattermostService < ChatNotificationService
 
   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: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
+      { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
       { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
 
   def default_channel_placeholder
-    "town-square"
+    "Channel handle (e.g. town-square)"
   end
 end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8d581a1f678f3f3f2fe058e4786e9ac8b66e20c
--- /dev/null
+++ b/app/models/project_services/mock_ci_service.rb
@@ -0,0 +1,82 @@
+# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
+class MockCiService < CiService
+  ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
+
+  prop_accessor :mock_service_url
+  validates :mock_service_url, presence: true, url: true, if: :activated?
+
+  def title
+    'MockCI'
+  end
+
+  def description
+    'Mock an external CI'
+  end
+
+  def self.to_param
+    'mock_ci'
+  end
+
+  def fields
+    [
+      { type: 'text',
+        name: 'mock_service_url',
+        placeholder: 'http://localhost:4004' },
+    ]
+  end
+
+  # Return complete url to build page
+  #
+  # Ex.
+  #   http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
+  #
+  def build_page(sha, ref)
+    url = [mock_service_url,
+           "#{project.namespace.path}/#{project.path}/status/#{sha}"]
+
+    URI.join(*url).to_s
+  end
+
+  # Return string with build status or :error symbol
+  #
+  # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+  #
+  #
+  # Ex.
+  #   @service.commit_status('13be4ac', 'master')
+  #   # => 'success'
+  #
+  #   @service.commit_status('2abe4ac', 'dev')
+  #   # => 'running'
+  #
+  #
+  def commit_status(sha, ref)
+    response = HTTParty.get(commit_status_path(sha), verify: false)
+    read_commit_status(response)
+  rescue Errno::ECONNREFUSED
+    :error
+  end
+
+  def commit_status_path(sha)
+    url = [mock_service_url,
+           "#{project.namespace.path}/#{project.path}/status/#{sha}.json"]
+
+    URI.join(*url).to_s
+  end
+
+  def read_commit_status(response)
+    return :error unless response.code == 200 || response.code == 404
+
+    status = if response.code == 404
+               'pending'
+             else
+               response['status']
+             end
+
+    if status.present? && ALLOWED_STATES.include?(status)
+      status
+    else
+      :error
+    end
+  end
+end
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea585721e8f1dc92398b7651d50f2eea3cb8360b
--- /dev/null
+++ b/app/models/project_services/monitoring_service.rb
@@ -0,0 +1,16 @@
+# Base class for monitoring services
+#
+# These services integrate with a deployment solution like Prometheus
+# to provide additional features for environments.
+class MonitoringService < Service
+  default_value_for :category, 'monitoring'
+
+  def self.supported_events
+    %w()
+  end
+
+  # Environments have a number of metrics
+  def metrics(environment)
+    raise NotImplementedError
+  end
+end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 9cc642591f4364104ad77e5ac253fe5dbaf10761..d86f4f6f4480cbc23f1fa03bcf29775ace73fd2c 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,7 @@
 class PivotaltrackerService < Service
   include HTTParty
 
-  API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+  API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze
 
   prop_accessor :token, :restrict_to_branch
   validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..375966b9efc61a8f11ef2cae1ac40fac0bffe827
--- /dev/null
+++ b/app/models/project_services/prometheus_service.rb
@@ -0,0 +1,93 @@
+class PrometheusService < MonitoringService
+  include ReactiveCaching
+
+  self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
+  self.reactive_cache_lease_timeout = 30.seconds
+  self.reactive_cache_refresh_interval = 30.seconds
+  self.reactive_cache_lifetime = 1.minute
+
+  #  Access to prometheus is directly through the API
+  prop_accessor :api_url
+
+  with_options presence: true, if: :activated? do
+    validates :api_url, url: true
+  end
+
+  after_save :clear_reactive_cache!
+
+  def initialize_properties
+    if properties.nil?
+      self.properties = {}
+    end
+  end
+
+  def title
+    'Prometheus'
+  end
+
+  def description
+    'Prometheus monitoring'
+  end
+
+  def help
+    'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
+  end
+
+  def self.to_param
+    'prometheus'
+  end
+
+  def fields
+    [
+      {
+        type: 'text',
+        name: 'api_url',
+        title: 'API URL',
+        placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+      }
+    ]
+  end
+
+  # Check we can connect to the Prometheus API
+  def test(*args)
+    client.ping
+
+    { success: true, result: 'Checked API endpoint' }
+  rescue Gitlab::PrometheusError => err
+    { success: false, result: err }
+  end
+
+  def metrics(environment)
+    with_reactive_cache(environment.slug) do |data|
+      data
+    end
+  end
+
+  # Cache metrics for specific environment
+  def calculate_reactive_cache(environment_slug)
+    return unless active? && project && !project.pending_delete?
+
+    memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
+    cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+
+    {
+      success: true,
+      metrics: {
+        # Memory used in MB
+        memory_values: client.query_range(memory_query, start: 8.hours.ago),
+        memory_current: client.query(memory_query),
+        # CPU Usage rate in cores.
+        cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
+        cpu_current: client.query(cpu_query)
+      },
+      last_update: Time.now.utc
+    }
+
+  rescue Gitlab::PrometheusError => err
+    { success: false, result: err.message }
+  end
+
+  def client
+    @prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
+  end
+end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index a963d27a37652e491cd712115364babbc52f942d..3e618a8dbf1a86db492dc1819df6f685ef784a10 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -29,25 +29,24 @@ class PushoverService < Service
           ['Normal Priority', 0],
           ['High Priority', 1]
         ],
-        default_choice: 0
-      },
+        default_choice: 0 },
       { type: 'select', name: 'sound', choices:
         [
           ['Device default sound', nil],
           ['Pushover (default)', 'pushover'],
-          ['Bike', 'bike'],
-          ['Bugle', 'bugle'],
+          %w(Bike bike),
+          %w(Bugle bugle),
           ['Cash Register', 'cashregister'],
-          ['Classical', 'classical'],
-          ['Cosmic', 'cosmic'],
-          ['Falling', 'falling'],
-          ['Gamelan', 'gamelan'],
-          ['Incoming', 'incoming'],
-          ['Intermission', 'intermission'],
-          ['Magic', 'magic'],
-          ['Mechanical', 'mechanical'],
+          %w(Classical classical),
+          %w(Cosmic cosmic),
+          %w(Falling falling),
+          %w(Gamelan gamelan),
+          %w(Incoming incoming),
+          %w(Intermission intermission),
+          %w(Magic magic),
+          %w(Mechanical mechanical),
           ['Piano Bar', 'pianobar'],
-          ['Siren', 'siren'],
+          %w(Siren siren),
           ['Space Alarm', 'spacealarm'],
           ['Tug Boat', 'tugboat'],
           ['Alien Alarm (long)', 'alien'],
@@ -56,8 +55,7 @@ class PushoverService < Service
           ['Pushover Echo (long)', 'echo'],
           ['Up Down (long)', 'updown'],
           ['None (silent)', 'none']
-        ]
-      },
+        ] },
     ]
   end
 
@@ -72,13 +70,14 @@ class PushoverService < Service
     before = data[:before]
     after = data[:after]
 
-    if Gitlab::Git.blank_ref?(before)
-      message = "#{data[:user_name]} pushed new branch \"#{ref}\"."
-    elsif Gitlab::Git.blank_ref?(after)
-      message = "#{data[:user_name]} deleted branch \"#{ref}\"."
-    else
-      message = "#{data[:user_name]} push to branch \"#{ref}\"."
-    end
+    message =
+      if Gitlab::Git.blank_ref?(before)
+        "#{data[:user_name]} pushed new branch \"#{ref}\"."
+      elsif Gitlab::Git.blank_ref?(after)
+        "#{data[:user_name]} deleted branch \"#{ref}\"."
+      else
+        "#{data[:user_name]} push to branch \"#{ref}\"."
+      end
 
     if data[:total_commits_count] > 0
       message << "\nTotal commits count: #{data[:total_commits_count]}"
@@ -97,7 +96,7 @@ class PushoverService < Service
 
     # Sound parameter MUST NOT be sent to API if not selected
     if sound
-      pushover_data.merge!(sound: sound)
+      pushover_data[:sound] = sound
     end
 
     PushoverService.post('/messages.json', body: pushover_data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index f77d2d7c60ba925c6eaba1ce41e777b85dc9f03d..b657db6f9ee126d3b7866e5ea27963174127152e 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
 
   def help
     'This service sends notifications about projects events to Slack channels.<br />
-    To setup this service:
+    To set up 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>
+      <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 <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
     </ol>'
   end
 
@@ -27,14 +27,13 @@ class SlackService < ChatNotificationService
 
   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: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
+      { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
       { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
 
   def default_channel_placeholder
-    "#general"
+    "Channel name (e.g. general)"
   end
 end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 06abd4065239a8c60de134f463042cea1bb1642a..aeaf63abab99510c6e537809ed77cd049ebcaeef 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,7 +4,7 @@ class ProjectStatistics < ActiveRecord::Base
 
   before_save :update_storage_size
 
-  STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
+  STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
   STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
 
   def total_repository_size
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index d0b991db11288ee0c42c67a0c3e29569645c0fbb..70eef359cdd0920c1586211919ef16c7fc38ecf7 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -5,9 +5,9 @@ class ProjectWiki
     'Markdown' => :markdown,
     'RDoc'     => :rdoc,
     'AsciiDoc' => :asciidoc
-  } unless defined?(MARKUPS)
+  }.freeze unless defined?(MARKUPS)
 
-  class CouldNotCreateWikiError < StandardError; end
+  CouldNotCreateWikiError = Class.new(StandardError)
 
   # Returns a string describing what went wrong after
   # an operation fails.
@@ -19,6 +19,9 @@ class ProjectWiki
     @user = user
   end
 
+  delegate :empty?, to: :pages
+  delegate :repository_storage_path, to: :project
+
   def path
     @project.path + '.wiki'
   end
@@ -39,8 +42,11 @@ class ProjectWiki
     url_to_repo
   end
 
-  def http_url_to_repo
-    [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
+  def http_url_to_repo(user = nil)
+    url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
+    credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+    Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
   end
 
   def wiki_base_path
@@ -60,10 +66,6 @@ class ProjectWiki
     !!repository.exists?
   end
 
-  def empty?
-    pages.empty?
-  end
-
   # Returns an Array of Gitlab WikiPage instances or an
   # empty Array if this Wiki has no pages.
   def pages
@@ -160,10 +162,6 @@ class ProjectWiki
     }
   end
 
-  def repository_storage_path
-    project.repository_storage_path
-  end
-
   private
 
   def init_repo(path_with_namespace)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 6240912a6e1aff65b4e0daeaeea4d4d92329690a..39e979ef15b318fe91dee58152748753c8f2cec3 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -8,8 +8,8 @@ class ProtectedBranch < ActiveRecord::Base
   has_many :merge_access_levels, dependent: :destroy
   has_many :push_access_levels, dependent: :destroy
 
-  validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
-  validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+  validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
+  validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
 
   accepts_nested_attributes_for :push_access_levels
   accepts_nested_attributes_for :merge_access_levels
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 56c582cd9be88635cc6b2cb9301996eaa4ff843c..6ab04440ca876297977c54aa0bae864d9f4c2aee 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -6,6 +6,7 @@ class Repository
   attr_accessor :path_with_namespace, :project
 
   CommitError = Class.new(StandardError)
+  CreateTreeError = Class.new(StandardError)
 
   # Methods that cache data from the Git repository.
   #
@@ -18,7 +19,7 @@ class Repository
   CACHED_METHODS = %i(size commit_count readme version contribution_guide
                       changelog license_blob license_key gitignore koding_yml
                       gitlab_ci_yml branch_names tag_names branch_count
-                      tag_count avatar exists? empty? root_ref)
+                      tag_count avatar exists? empty? root_ref).freeze
 
   # Certain method caches should be refreshed when certain types of files are
   # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
@@ -33,7 +34,7 @@ class Repository
     koding: :koding_yml,
     gitlab_ci: :gitlab_ci_yml,
     avatar: :avatar
-  }
+  }.freeze
 
   # Wraps around the given method and caches its output in Redis and an instance
   # variable.
@@ -49,10 +50,6 @@ class Repository
     end
   end
 
-  def self.storages
-    Gitlab.config.repositories.storages
-  end
-
   def initialize(path_with_namespace, project)
     @path_with_namespace = path_with_namespace
     @project = project
@@ -109,9 +106,7 @@ class Repository
       offset: offset,
       after: after,
       before: before,
-      # --follow doesn't play well with --skip. See:
-      # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
-      follow: false,
+      follow: path.present?,
       skip_merges: skip_merges
     }
 
@@ -317,11 +312,13 @@ class Repository
     if !branch_name || branch_name == root_ref
       branches.each do |branch|
         cache.expire(:"diverging_commit_counts_#{branch.name}")
+        cache.expire(:"commit_count_#{branch.name}")
       end
     # In case a commit is pushed to a non-root branch we only have to flush the
     # cache for said branch.
     else
       cache.expire(:"diverging_commit_counts_#{branch_name}")
+      cache.expire(:"commit_count_#{branch_name}")
     end
   end
 
@@ -487,9 +484,7 @@ class Repository
   end
   cache_method :exists?
 
-  def empty?
-    raw_repository.empty?
-  end
+  delegate :empty?, to: :raw_repository
   cache_method :empty?
 
   # The size of this repository in megabytes.
@@ -503,14 +498,22 @@ class Repository
   end
   cache_method :commit_count, fallback: 0
 
+  def commit_count_for_ref(ref)
+    return 0 unless exists?
+
+    begin
+      cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) }
+    rescue Rugged::ReferenceError
+      0
+    end
+  end
+
   def branch_names
     branches.map(&:name)
   end
   cache_method :branch_names, fallback: []
 
-  def tag_names
-    raw_repository.tag_names
-  end
+  delegate :tag_names, to: :raw_repository
   cache_method :tag_names, fallback: []
 
   def branch_count
@@ -750,136 +753,63 @@ class Repository
     @tags ||= raw_repository.tags
   end
 
-  # rubocop:disable Metrics/ParameterLists
-  def commit_dir(
-    user, path,
-    message:, branch_name:,
-    author_email: nil, author_name: nil,
-    start_branch_name: nil, start_project: project)
-    check_tree_entry_for_dir(branch_name, path)
-
-    if start_branch_name
-      start_project.repository.
-        check_tree_entry_for_dir(start_branch_name, path)
-    end
+  def create_dir(user, path, **options)
+    options[:user] = user
+    options[:actions] = [{ action: :create_dir, file_path: path }]
 
-    commit_file(
-      user,
-      "#{path}/.gitkeep",
-      '',
-      message: message,
-      branch_name: branch_name,
-      update: false,
-      author_email: author_email,
-      author_name: author_name,
-      start_branch_name: start_branch_name,
-      start_project: start_project)
+    multi_action(**options)
   end
-  # rubocop:enable Metrics/ParameterLists
 
-  # rubocop:disable Metrics/ParameterLists
-  def commit_file(
-    user, path, content,
-    message:, branch_name:, update: true,
-    author_email: nil, author_name: nil,
-    start_branch_name: nil, start_project: project)
-    unless update
-      error_message = "Filename already exists; update not allowed"
+  def create_file(user, path, content, **options)
+    options[:user] = user
+    options[:actions] = [{ action: :create, file_path: path, content: content }]
 
-      if tree_entry_at(branch_name, path)
-        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
-      end
+    multi_action(**options)
+  end
 
-      if start_branch_name &&
-          start_project.repository.tree_entry_at(start_branch_name, path)
-        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
-      end
-    end
+  def update_file(user, path, content, **options)
+    previous_path = options.delete(:previous_path)
+    action = previous_path && previous_path != path ? :move : :update
 
-    multi_action(
-      user: user,
-      message: message,
-      branch_name: branch_name,
-      author_email: author_email,
-      author_name: author_name,
-      start_branch_name: start_branch_name,
-      start_project: start_project,
-      actions: [{ action: :create,
-                  file_path: path,
-                  content: content }])
-  end
-  # rubocop:enable Metrics/ParameterLists
+    options[:user] = user
+    options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
 
-  # rubocop:disable Metrics/ParameterLists
-  def update_file(
-    user, path, content,
-    message:, branch_name:, previous_path:,
-    author_email: nil, author_name: nil,
-    start_branch_name: nil, start_project: project)
-    action = if previous_path && previous_path != path
-               :move
-             else
-               :update
-             end
-
-    multi_action(
-      user: user,
-      message: message,
-      branch_name: branch_name,
-      author_email: author_email,
-      author_name: author_name,
-      start_branch_name: start_branch_name,
-      start_project: start_project,
-      actions: [{ action: action,
-                  file_path: path,
-                  content: content,
-                  previous_path: previous_path }])
+    multi_action(**options)
   end
-  # rubocop:enable Metrics/ParameterLists
 
-  # rubocop:disable Metrics/ParameterLists
-  def remove_file(
-    user, path,
-    message:, branch_name:,
-    author_email: nil, author_name: nil,
-    start_branch_name: nil, start_project: project)
-    multi_action(
-      user: user,
-      message: message,
-      branch_name: branch_name,
-      author_email: author_email,
-      author_name: author_name,
-      start_branch_name: start_branch_name,
-      start_project: start_project,
-      actions: [{ action: :delete,
-                  file_path: path }])
+  def delete_file(user, path, **options)
+    options[:user] = user
+    options[:actions] = [{ action: :delete, file_path: path }]
+
+    multi_action(**options)
   end
-  # rubocop:enable Metrics/ParameterLists
 
   # rubocop:disable Metrics/ParameterLists
   def multi_action(
     user:, branch_name:, message:, actions:,
     author_email: nil, author_name: nil,
     start_branch_name: nil, start_project: project)
+
     GitOperationService.new(user, self).with_branch(
       branch_name,
       start_branch_name: start_branch_name,
       start_project: start_project) do |start_commit|
-      index = rugged.index
 
-      parents = if start_commit
-                  index.read_tree(start_commit.raw_commit.tree)
-                  [start_commit.sha]
-                else
-                  []
-                end
+      index = Gitlab::Git::Index.new(raw_repository)
 
-      actions.each do |act|
-        git_action(index, act)
+      if start_commit
+        index.read_tree(start_commit.raw_commit.tree)
+        parents = [start_commit.sha]
+      else
+        parents = []
+      end
+
+      actions.each do |options|
+        index.public_send(options.delete(:action), options)
       end
 
       options = {
-        tree: index.write_tree(rugged),
+        tree: index.write_tree,
         message: message,
         parents: parents
       }
@@ -892,7 +822,7 @@ class Repository
 
   def get_committer_and_author(user, email: nil, name: nil)
     committer = user_to_committer(user)
-    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
+    author = Gitlab::Git.committer_hash(email: email, name: name) || committer
 
     {
       author: author,
@@ -941,17 +871,18 @@ class Repository
   end
 
   def revert(
-    user, commit, branch_name, revert_tree_id = nil,
+    user, commit, branch_name,
     start_branch_name: nil, start_project: project)
-    revert_tree_id ||= check_revert_content(commit, branch_name)
-
-    return false unless revert_tree_id
-
     GitOperationService.new(user, self).with_branch(
       branch_name,
       start_branch_name: start_branch_name,
       start_project: start_project) do |start_commit|
 
+      revert_tree_id = check_revert_content(commit, start_commit.sha)
+      unless revert_tree_id
+        raise Repository::CreateTreeError.new('Failed to revert commit')
+      end
+
       committer = user_to_committer(user)
 
       Rugged::Commit.create(rugged,
@@ -964,17 +895,18 @@ class Repository
   end
 
   def cherry_pick(
-    user, commit, branch_name, cherry_pick_tree_id = nil,
+    user, commit, branch_name,
     start_branch_name: nil, start_project: project)
-    cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
-
-    return false unless cherry_pick_tree_id
-
     GitOperationService.new(user, self).with_branch(
       branch_name,
       start_branch_name: start_branch_name,
       start_project: start_project) do |start_commit|
 
+      cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
+      unless cherry_pick_tree_id
+        raise Repository::CreateTreeError.new('Failed to cherry-pick commit')
+      end
+
       committer = user_to_committer(user)
 
       Rugged::Commit.create(rugged,
@@ -998,9 +930,8 @@ class Repository
     end
   end
 
-  def check_revert_content(target_commit, branch_name)
-    source_sha = commit(branch_name).sha
-    args       = [target_commit.sha, source_sha]
+  def check_revert_content(target_commit, source_sha)
+    args = [target_commit.sha, source_sha]
     args << { mainline: 1 } if target_commit.merge_commit?
 
     revert_index = rugged.revert_commit(*args)
@@ -1012,9 +943,8 @@ class Repository
     tree_id
   end
 
-  def check_cherry_pick_content(target_commit, branch_name)
-    source_sha = commit(branch_name).sha
-    args       = [target_commit.sha, source_sha]
+  def check_cherry_pick_content(target_commit, source_sha)
+    args = [target_commit.sha, source_sha]
     args << 1 if target_commit.merge_commit?
 
     cherry_pick_index = rugged.cherrypick_commit(*args)
@@ -1074,6 +1004,8 @@ class Repository
   end
 
   def with_repo_branch_commit(start_repository, start_branch_name)
+    return yield(nil) if start_repository.empty_repo?
+
     branch_name_or_sha =
       if start_repository == self
         start_branch_name
@@ -1170,30 +1102,6 @@ class Repository
     blob_data_at(sha, '.gitlab-ci.yml')
   end
 
-  protected
-
-  def tree_entry_at(branch_name, path)
-    branch_exists?(branch_name) &&
-      # tree_entry is private
-      raw_repository.send(:tree_entry, commit(branch_name), path)
-  end
-
-  def check_tree_entry_for_dir(branch_name, path)
-    return unless branch_exists?(branch_name)
-
-    entry = tree_entry_at(branch_name, path)
-
-    return unless entry
-
-    if entry[:type] == :blob
-      raise Gitlab::Git::Repository::InvalidBlobName.new(
-        "Directory already exists as a file")
-    else
-      raise Gitlab::Git::Repository::InvalidBlobName.new(
-        "Directory already exists")
-    end
-  end
-
   private
 
   def blob_data_at(sha, path)
@@ -1204,58 +1112,6 @@ class Repository
     blob.data
   end
 
-  def git_action(index, action)
-    path = normalize_path(action[:file_path])
-
-    if action[:action] == :move
-      previous_path = normalize_path(action[:previous_path])
-    end
-
-    case action[:action]
-    when :create, :update, :move
-      mode =
-        case action[:action]
-        when :update
-          index.get(path)[:mode]
-        when :move
-          index.get(previous_path)[:mode]
-        end
-      mode ||= 0o100644
-
-      index.remove(previous_path) if action[:action] == :move
-
-      content = if action[:encoding] == 'base64'
-                  Base64.decode64(action[:content])
-                else
-                  action[:content]
-                end
-
-      detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
-
-      unless detect && detect[:type] == :binary
-        # When writing to the repo directly as we are doing here,
-        # the `core.autocrlf` config isn't taken into account.
-        content.gsub!("\r\n", "\n") if self.autocrlf
-      end
-
-      oid = rugged.write(content, :blob)
-
-      index.add(path: path, oid: oid, mode: mode)
-    when :delete
-      index.remove(path)
-    end
-  end
-
-  def normalize_path(path)
-    pathname = Gitlab::Git::PathHelper.normalize_path(path)
-
-    if pathname.each_filename.include?('..')
-      raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
-    end
-
-    pathname.to_s
-  end
-
   def refs_directory_exists?
     return false unless path_with_namespace
 
diff --git a/app/models/route.rb b/app/models/route.rb
index 73574a6206b6e1d21918f6ab37e0437a66de42c0..41e6eb7cb732c2c195c617abf39052b0dd6a26d2 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base
           attributes[:path] = route.path.sub(path_was, path)
         end
 
-        if name_changed? && route.name.present?
+        if name_changed? && name_was.present? && route.name.present?
           attributes[:name] = route.name.sub(name_was, name)
         end
 
diff --git a/app/models/service.rb b/app/models/service.rb
index facaaf9b331e02182370af813574a027c64d4c4f..e73f7e5d1a3530b8279c6d052a772bd6a7b225a9 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -210,12 +210,11 @@ class Service < ActiveRecord::Base
   end
 
   def self.available_services_names
-    %w[
+    service_names = %w[
       asana
       assembla
       bamboo
       buildkite
-      builds_email
       bugzilla
       campfire
       custom_issue_tracker
@@ -232,12 +231,16 @@ class Service < ActiveRecord::Base
       mattermost
       pipelines_email
       pivotaltracker
+      prometheus
       pushover
       redmine
       slack_slash_commands
       slack
       teamcity
     ]
+    service_names << 'mock_ci' if Rails.env.development?
+
+    service_names.sort_by(&:downcase)
   end
 
   def self.build_from_template(project_id, template)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2665a7249a353b1f2a14c69d78222ad1bd47b527..dbd564e5e7d7e772934b6754e7a7514dc16fd242 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -120,7 +120,7 @@ class Snippet < ActiveRecord::Base
   end
 
   def visibility_level_field
-    visibility_level
+    :visibility_level
   end
 
   def no_highlighting?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3dda7948d0b25e356a422630be295b38d3588b75..da3fa7277c2c54488dddec023703c24717f942cf 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -17,7 +17,7 @@ class Todo < ActiveRecord::Base
     APPROVAL_REQUIRED => :approval_required,
     UNMERGEABLE => :unmergeable,
     DIRECTLY_ADDRESSED => :directly_addressed
-  }
+  }.freeze
 
   belongs_to :author, class_name: "User"
   belongs_to :note
@@ -48,8 +48,14 @@ class Todo < ActiveRecord::Base
   after_save :keep_around_commit
 
   class << self
+    # Priority sorting isn't displayed in the dropdown, because we don't show
+    # milestones, but still show something if the user has a URL with that
+    # selected.
     def sort(method)
-      method == "priority" ? order_by_labels_priority : order_by(method)
+      case method.to_s
+      when 'priority', 'label_priority' then order_by_labels_priority
+      else order_by(method)
+      end
     end
 
     # Order by priority depending on which issue/merge request the Todo belongs to
diff --git a/app/models/upload.rb b/app/models/upload.rb
new file mode 100644
index 0000000000000000000000000000000000000000..13987931b0505820cfb4b55268f4ac39d36821b0
--- /dev/null
+++ b/app/models/upload.rb
@@ -0,0 +1,63 @@
+class Upload < ActiveRecord::Base
+  # Upper limit for foreground checksum processing
+  CHECKSUM_THRESHOLD = 100.megabytes
+
+  belongs_to :model, polymorphic: true
+
+  validates :size, presence: true
+  validates :path, presence: true
+  validates :model, presence: true
+  validates :uploader, presence: true
+
+  before_save  :calculate_checksum, if:     :foreground_checksum?
+  after_commit :schedule_checksum,  unless: :foreground_checksum?
+
+  def self.remove_path(path)
+    where(path: path).destroy_all
+  end
+
+  def self.record(uploader)
+    remove_path(uploader.relative_path)
+
+    create(
+      size: uploader.file.size,
+      path: uploader.relative_path,
+      model: uploader.model,
+      uploader: uploader.class.to_s
+    )
+  end
+
+  def absolute_path
+    return path unless relative_path?
+
+    uploader_class.absolute_path(self)
+  end
+
+  def calculate_checksum
+    return unless exist?
+
+    self.checksum = Digest::SHA256.file(absolute_path).hexdigest
+  end
+
+  def exist?
+    File.exist?(absolute_path)
+  end
+
+  private
+
+  def foreground_checksum?
+    size <= CHECKSUM_THRESHOLD
+  end
+
+  def schedule_checksum
+    UploadChecksumWorker.perform_async(id)
+  end
+
+  def relative_path?
+    !path.start_with?('/')
+  end
+
+  def uploader_class
+    Object.const_get(uploader)
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index f614eb66e1fe5f1af31140bf116347fd87c4559e..8c7ad5d51741f3e823c7033ba5e71cc3a86c9563 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,6 +21,7 @@ class User < ActiveRecord::Base
   default_value_for :can_create_team, false
   default_value_for :hide_no_ssh_key, false
   default_value_for :hide_no_password, false
+  default_value_for :project_view, :files
 
   attr_encrypted :otp_secret,
     key:       Gitlab::Application.secrets.otp_key_base,
@@ -81,7 +82,6 @@ class User < ActiveRecord::Base
   has_many :authorized_projects, through: :project_authorizations, source: :project
 
   has_many :snippets,                 dependent: :destroy, foreign_key: :author_id
-  has_many :issues,                   dependent: :destroy, foreign_key: :author_id
   has_many :notes,                    dependent: :destroy, foreign_key: :author_id
   has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id
   has_many :events,                   dependent: :destroy, foreign_key: :author_id
@@ -95,16 +95,22 @@ class User < ActiveRecord::Base
   has_many :todos,                    dependent: :destroy
   has_many :notification_settings,    dependent: :destroy
   has_many :award_emoji,              dependent: :destroy
+  has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
 
   has_many :assigned_issues,          dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
   has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
 
+  # Issues that a user owns are expected to be moved to the "ghost" user before
+  # the user is destroyed. If the user owns any issues during deletion, this
+  # should be treated as an exceptional condition.
+  has_many :issues,                   dependent: :restrict_with_exception, foreign_key: :author_id
+
   #
   # Validations
   #
   # Note: devise :validatable above adds validations for :email and :password
   validates :name, presence: true
-  validates_confirmation_of :email
+  validates :email, confirmation: true
   validates :notification_email, presence: true
   validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
   validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
@@ -184,6 +190,7 @@ class User < ActiveRecord::Base
   end
 
   mount_uploader :avatar, AvatarUploader
+  has_many :uploads, as: :model, dependent: :destroy
 
   # Scopes
   scope :admins, -> { where(admin: true) }
@@ -317,8 +324,7 @@ class User < ActiveRecord::Base
     end
 
     def find_by_personal_access_token(token_string)
-      personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
-      personal_access_token&.user
+      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
     end
 
     # Returns a user for the given SSH key.
@@ -334,9 +340,34 @@ class User < ActiveRecord::Base
     def reference_pattern
       %r{
         #{Regexp.escape(reference_prefix)}
-        (?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR})
+        (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
       }x
     end
+
+    # Return (create if necessary) the ghost user. The ghost user
+    # owns records previously belonging to deleted users.
+    def ghost
+      unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u|
+        u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
+        u.name = 'Ghost User'
+      end
+    end
+  end
+
+  def self.internal_attributes
+    [:ghost]
+  end
+
+  def internal?
+    self.class.internal_attributes.any? { |a| self[a] }
+  end
+
+  def self.internal
+    where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
+  end
+
+  def self.non_internal
+    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
   end
 
   #
@@ -457,7 +488,7 @@ class User < ActiveRecord::Base
     Group.member_descendants(id)
   end
 
-  def nested_projects
+  def nested_groups_projects
     Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
       member_descendants(id)
   end
@@ -540,14 +571,14 @@ class User < ActiveRecord::Base
   end
 
   def can_create_group?
-    can?(:create_group, nil)
+    can?(:create_group)
   end
 
   def can_select_namespace?
     several_namespaces? || admin
   end
 
-  def can?(action, subject)
+  def can?(action, subject = :global)
     Ability.allowed?(self, action, subject)
   end
 
@@ -580,8 +611,8 @@ class User < ActiveRecord::Base
 
       if project.repository.branch_exists?(event.branch_name)
         merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
-            where(source_project_id: project.id,
-                  source_branch: event.branch_name)
+          where(source_project_id: project.id,
+                source_branch: event.branch_name)
         merge_requests.empty?
       end
     end
@@ -846,7 +877,7 @@ class User < ActiveRecord::Base
   def ci_authorized_runners
     @ci_authorized_runners ||= begin
       runner_ids = Ci::RunnerProject.
-        where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+        where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
         select(:runner_id)
       Ci::Runner.specific.where(id: runner_ids)
     end
@@ -932,6 +963,14 @@ class User < ActiveRecord::Base
     self.admin = (new_level == 'admin')
   end
 
+  protected
+
+  # override, from Devise::Validatable
+  def password_required?
+    return false if internal?
+    super
+  end
+
   private
 
   def ci_projects_union
@@ -999,4 +1038,43 @@ class User < ActiveRecord::Base
       super
     end
   end
+
+  def self.unique_internal(scope, username, email_pattern, &b)
+    scope.first || create_unique_internal(scope, username, email_pattern, &b)
+  end
+
+  def self.create_unique_internal(scope, username, email_pattern, &creation_block)
+    # Since we only want a single one of these in an instance, we use an
+    # exclusive lease to ensure than this block is never run concurrently.
+    lease_key = "user:unique_internal:#{username}"
+    lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
+
+    until uuid = lease.try_obtain
+      # Keep trying until we obtain the lease. To prevent hammering Redis too
+      # much we'll wait for a bit between retries.
+      sleep(1)
+    end
+
+    # Recheck if the user is already present. One might have been
+    # added between the time we last checked (first line of this method)
+    # and the time we acquired the lock.
+    existing_user = uncached { scope.first }
+    return existing_user if existing_user.present?
+
+    uniquify = Uniquify.new
+
+    username = uniquify.string(username) { |s| User.find_by_username(s) }
+
+    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
+      User.find_by_email(s)
+    end
+
+    scope.create(
+      username: username,
+      email: email,
+      &creation_block
+    )
+  ensure
+    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+  end
 end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2caebb496db2d8934fa11bb27237ee7c11966442..c771c22f46a1620b37986d738eedac588be66cfc 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -149,7 +149,13 @@ class WikiPage
   end
 
   # Returns boolean True or False if this instance
-  # has been fully saved to disk or not.
+  # is the latest commit version of the page.
+  def latest?
+    !historical?
+  end
+
+  # Returns boolean True or False if this instance
+  # has been fully created on disk or not.
   def persisted?
     @persisted == true
   end
@@ -220,6 +226,8 @@ class WikiPage
   end
 
   def save(method, *args)
+    saved = false
+
     project_wiki = wiki
     if valid? && project_wiki.send(method, *args)
 
@@ -237,10 +245,10 @@ class WikiPage
       set_attributes
 
       @persisted = true
+      saved = true
     else
       errors.add(:base, project_wiki.error_message) if project_wiki.error_message
-      @persisted = false
     end
-    @persisted
+    saved
   end
 end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index b9f1c29c32ee615dd6eaf6ccdb7b905145126f85..8890409d0565e8660af28c96485b1ad82eb1419d 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -6,14 +6,16 @@ class BasePolicy
       @cannot_set = cannot_set
     end
 
-    def size
-      to_set.size
-    end
+    delegate :size, to: :to_set
 
     def self.empty
       new(Set.new, Set.new)
     end
 
+    def self.none
+      empty.freeze
+    end
+
     def can?(ability)
       @can_set.include?(ability) && !@cannot_set.include?(ability)
     end
@@ -51,7 +53,8 @@ class BasePolicy
   end
 
   def self.class_for(subject)
-    return GlobalPolicy if subject.nil?
+    return GlobalPolicy if subject == :global
+    raise ArgumentError, 'no policy for nil' if subject.nil?
 
     if subject.class.try(:presenter?)
       subject = subject.subject
@@ -81,7 +84,7 @@ class BasePolicy
   end
 
   def abilities
-    return RuleSet.empty if @user && @user.blocked?
+    return RuleSet.none if @user && @user.blocked?
     return anonymous_abilities if @user.nil?
     collect_rules { rules }
   end
diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c90c9ac058374f5ca580a733d7d1f1dbfef353f6
--- /dev/null
+++ b/app/policies/ci/trigger_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+  class TriggerPolicy < BasePolicy
+    def rules
+      delegate! @subject.project
+
+      if can?(:admin_build)
+        can! :admin_trigger if @subject.owner.blank? ||
+            @subject.owner == @user
+        can! :manage_trigger
+      end
+    end
+  end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 3c2fbe6b56baa58386384ef32b957684a5a203ec..cb72c2b4590d2a300128ff47d598f1f21c712b6a 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy
 
     can! :create_group if @user.can_create_group
     can! :read_users_list
+
+    unless @user.blocked? || @user.internal?
+      can! :log_in unless @user.access_locked?
+      can! :access_api
+      can! :access_git
+      can! :receive_notifications
+    end
   end
 end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 0be6e1136558be7fa339bcb63f5a083d842045a3..4cc21696eb689270dbba9ec6798561a5e05ecebd 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -33,8 +33,6 @@ class GroupPolicy < BasePolicy
     if globally_viewable && @subject.request_access_enabled && !member
       can! :request_access
     end
-
-    additional_rules!(master)
   end
 
   def can_read_group?
@@ -45,8 +43,4 @@ class GroupPolicy < BasePolicy
 
     GroupProjectsFinder.new(@subject).execute(@user).any?
   end
-
-  def additional_rules!(master)
-    # This is meant to be overriden in EE
-  end
 end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 03a2499e2638b2e46401b1f3c8a152c67ebac6ea..229846e368c50f8661a79fe475fec242207f5312 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -3,6 +3,14 @@ class UserPolicy < BasePolicy
 
   def rules
     can! :read_user if @user || !restricted_public_level?
+
+    if @user
+      if @user.admin? || @subject == @user
+        can! :destroy_user
+      end
+
+      cannot! :destroy_user if @subject.ghost?
+    end
   end
 
   def restricted_public_level?
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86ac513b3c07e1a9210894d1cd7b7627e27e7f2c
--- /dev/null
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -0,0 +1,60 @@
+module Projects
+  module Settings
+    class DeployKeysPresenter < Gitlab::View::Presenter::Simple
+      presents :project
+      delegate :size, to: :enabled_keys, prefix: true
+      delegate :size, to: :available_project_keys, prefix: true
+      delegate :size, to: :available_public_keys, prefix: true
+
+      def new_key
+        @key ||= DeployKey.new
+      end
+
+      def enabled_keys
+        @enabled_keys ||= project.deploy_keys
+      end
+
+      def any_keys_enabled?
+        enabled_keys.any?
+      end
+
+      def available_keys
+        @available_keys ||= current_user.accessible_deploy_keys - enabled_keys
+      end
+
+      def available_project_keys
+        @available_project_keys ||= current_user.project_deploy_keys - enabled_keys
+      end
+
+      def any_available_project_keys_enabled?
+        available_project_keys.any?
+      end
+
+      def key_available?(deploy_key)
+        available_keys.include?(deploy_key)
+      end
+
+      def available_public_keys
+        return @available_public_keys if defined?(@available_public_keys)
+
+        @available_public_keys ||= DeployKey.are_public - enabled_keys
+
+        # Public keys that are already used by another accessible project are already
+        # in @available_project_keys.
+        @available_public_keys -= available_project_keys
+      end
+
+      def any_available_public_keys_enabled?
+        available_public_keys.any?
+      end
+
+      def to_partial_path
+        'projects/deploy_keys/index'
+      end
+
+      def form_partial_path
+        'projects/deploy_keys/form'
+      end
+    end
+  end
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index b5384e6462b2967398a4f94143fa31c8b0401a56..5bcbe285052ee07f25218b1419f935f6a1f64795 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity
     path_to(:retry_namespace_project_build, build)
   end
 
-  expose :play_path, if: ->(build, _) { build.manual? } do |build|
+  expose :play_path, if: ->(build, _) { build.playable? } do |build|
     path_to(:play_namespace_project_build, build)
   end
 
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 7445298c71455c53c5556be2866150fb9ec00e63..5f80ab397a9baa7b74649cbe9e2b713d22c4c92d 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -6,7 +6,7 @@ class MergeRequestEntity < IssuableEntity
   expose :merge_params
   expose :merge_status
   expose :merge_user_id
-  expose :merge_when_build_succeeds
+  expose :merge_when_pipeline_succeeds
   expose :source_branch
   expose :source_project_id
   expose :target_branch
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 2bc6cf3266edadb9f7bd9c578239eec7b268a428..ab2d3d5a3ece51f5148f70d78b49bd3a056381bb 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,5 +1,5 @@
 class PipelineSerializer < BaseSerializer
-  class InvalidResourceError < StandardError; end
+  InvalidResourceError = Class.new(StandardError)
 
   entity PipelineEntity
 
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index ddaaed90e5befeec1f27f45c155bc8c3ca583a10..b2a543daa00ccae857a0aa41b29470b938955d44 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -1,10 +1,16 @@
-AccessTokenValidationService = Struct.new(:token) do
+class AccessTokenValidationService
   # Results:
   VALID = :valid
   EXPIRED = :expired
   REVOKED = :revoked
   INSUFFICIENT_SCOPE = :insufficient_scope
 
+  attr_reader :token
+
+  def initialize(token)
+    @token = token
+  end
+
   def validate(scopes: [])
     if token.expired?
       return EXPIRED
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 5b2fcdf3b16baf54ceec4b2124f67dcd483140e4..08fe6e3293ec61cd3dfa9cfb241365c81d0be362 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -2,7 +2,7 @@ module Auth
   class ContainerRegistryAuthenticationService < BaseService
     include Gitlab::CurrentSettings
 
-    AUDIENCE = 'container_registry'
+    AUDIENCE = 'container_registry'.freeze
 
     def execute(authentication_abilities:)
       @authentication_abilities = authentication_abilities
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index fa45506317ec819a0bf20ebe6de9f3bba0d94338..745c2c4b6813b45b4a3924898c00626a7875baaa 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -28,9 +28,7 @@ class BaseService
     SystemHooksService.new
   end
 
-  def repository
-    project.repository
-  end
+  delegate :repository, to: :project
 
   # Add an error to the specified model for restricted visibility levels
   def deny_visibility_level(model, denied_visibility_level = nil)
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 8a94c54b6ab8460dd6f295b9403b6ca38b6565a6..83f51947bd45fc0e8260d4360af807bafb0fb42d 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -5,7 +5,7 @@ module Boards
         issues = IssuesFinder.new(current_user, filter_params).execute
         issues = without_board_labels(issues) unless movable_list?
         issues = with_list_label(issues) if movable_list?
-        issues
+        issues.order_by_position_and_priority
       end
 
       private
@@ -26,7 +26,6 @@ module Boards
 
       def filter_params
         set_default_scope
-        set_default_sort
         set_project
         set_state
 
@@ -37,10 +36,6 @@ module Boards
         params[:scope] = 'all'
       end
 
-      def set_default_sort
-        params[:sort] = 'priority'
-      end
-
       def set_project
         params[:project_id] = project.id
       end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 96554a92a027ae83c5fc6519085d9c6b190cc536..2a9981ab88404cf4de21d52718158aa7ef94036f 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
     class MoveService < BaseService
       def execute(issue)
         return false unless can?(current_user, :update_issue, issue)
-        return false unless valid_move?
+        return false if issue_params.empty?
 
         update_service.execute(issue)
       end
@@ -14,7 +14,7 @@ module Boards
         @board ||= project.boards.find(params[:board_id])
       end
 
-      def valid_move?
+      def move_between_lists?
         moving_from_list.present? && moving_to_list.present? &&
           moving_from_list != moving_to_list
       end
@@ -32,11 +32,19 @@ module Boards
       end
 
       def issue_params
-        {
-          add_label_ids: add_label_ids,
-          remove_label_ids: remove_label_ids,
-          state_event: issue_state
-        }
+        attrs = {}
+
+        if move_between_lists?
+          attrs.merge!(
+            add_label_ids: add_label_ids,
+            remove_label_ids: remove_label_ids,
+            state_event: issue_state,
+          )
+        end
+
+        attrs[:move_between_iids] = move_between_iids if move_between_iids
+
+        attrs
       end
 
       def issue_state
@@ -58,6 +66,12 @@ module Boards
 
         Array(label_ids).compact
       end
+
+      def move_between_iids
+        return unless params[:move_after_iid] || params[:move_before_iid]
+
+        [params[:move_after_iid], params[:move_before_iid]]
+      end
     end
   end
 end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index b7da3f8e7eb04dbdf540608ded89e17119157c8d..70fb2c5e38f740b6968a5c4e3535cbe71cad14f5 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,9 +10,7 @@ module Ci
       end
     end
 
-    def project
-      pipeline.project
-    end
+    delegate :project, to: :pipeline
 
     private
 
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index e3bc9847200a27d5a976d7eb413fa83fe54d4518..38a85e9fc420178e6979e018bae7264d5a4de179 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -59,7 +59,8 @@ module Ci
     private
 
     def skip_ci?
-      pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
+      return false unless pipeline.git_commit_message
+      pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
     end
 
     def commit
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 6af3c1ca5b130d304bbb7b06f44f3877d931034a..dca5aa9f5d70260ae363640a903b2e0f91d9ac92 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -3,7 +3,7 @@ module Ci
     def execute(project, trigger, ref, variables = nil)
       trigger_request = trigger.trigger_requests.create(variables: variables)
 
-      pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+      pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
         execute(ignore_skip_ci: true, trigger_request: trigger_request)
       if pipeline.persisted?
         trigger_request
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
deleted file mode 100644
index 240ddabec36cf73e5ea64dfab68ee26c7a87acbf..0000000000000000000000000000000000000000
--- a/app/services/ci/image_for_build_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Ci
-  class ImageForBuildService
-    def execute(project, opts)
-      ref = opts[:ref]
-      sha = opts[:sha] || ref_sha(project, ref)
-      pipelines = project.pipelines.where(sha: sha)
-
-      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
-
-    private
-
-    def ref_sha(project, ref)
-      project.commit(ref).try(:sha) if ref
-    end
-
-    def image_for_status(status)
-      status ||= 'unknown'
-      'build-' + status + ".svg"
-    end
-  end
-end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 79eb97b7b551f7b6dd6557dc0b55e77e2e8f702d..2935d00c075f8284afa941d7322e9425d63d52f6 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -22,6 +22,8 @@ module Ci
     def process_stage(index)
       current_status = status_for_prior_stages(index)
 
+      return if HasStatus::BLOCKED_STATUS == current_status
+
       if HasStatus::COMPLETED_STATUSES.include?(current_status)
         created_builds_in_stage(index).select do |build|
           Gitlab::OptimisticLocking.retry_lock(build) do |subject|
@@ -33,7 +35,7 @@ module Ci
 
     def process_build(build, current_status)
       if valid_statuses_for_when(build.when).include?(current_status)
-        build.enqueue
+        build.action? ? build.actionize : build.enqueue
         true
       else
         build.skip
@@ -49,6 +51,8 @@ module Ci
         %w[failed]
       when 'always'
         %w[success failed skipped]
+      when 'manual'
+        %w[success]
       else
         []
       end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_job_service.rb
similarity index 51%
rename from app/services/ci/register_build_service.rb
rename to app/services/ci/register_job_service.rb
index 6f03bf2be13cf5e5250f12fc1cd9871203906472..d6a4280ce4c514cd487843c8d4f173e2b2e3a456 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -1,7 +1,7 @@
 module Ci
   # This class responsible for assigning
   # proper pending build to runner on runner API request
-  class RegisterBuildService
+  class RegisterJobService
     include Gitlab::CurrentSettings
 
     attr_reader :runner
@@ -20,21 +20,33 @@ module Ci
           builds_for_specific_runner
         end
 
-      build = builds.find do |build|
-        runner.can_pick?(build)
-      end
+      valid = true
 
-      if build
-        # In case when 2 runners try to assign the same build, second runner will be declined
-        # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
-        build.runner_id = runner.id
-        build.run!
-      end
+      builds.find do |build|
+        next unless runner.can_pick?(build)
+
+        begin
+          # In case when 2 runners try to assign the same build, second runner will be declined
+          # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
+          build.runner_id = runner.id
+          build.run!
 
-      Result.new(build, true)
+          return Result.new(build, true)
+        rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
+          # We are looping to find another build that is not conflicting
+          # It also indicates that this build can be picked and passed to runner.
+          # If we don't do it, basically a bunch of runners would be competing for a build
+          # and thus we will generate a lot of 409. This will increase
+          # the number of generated requests, also will reduce significantly
+          # how many builds can be picked by runner in a unit of time.
+          # In case we hit the concurrency-access lock,
+          # we still have to return 409 in the end,
+          # to make sure that this is properly handled by runner.
+          valid = false
+        end
+      end
 
-    rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
-      Result.new(build, false)
+      Result.new(nil, valid)
     end
 
     private
@@ -43,13 +55,13 @@ module Ci
       new_builds.
         # don't run projects which have not enabled shared runners and builds
         joins(:project).where(projects: { shared_runners_enabled: true }).
-        joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+        joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
         where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
 
         # Implement fair scheduling
         # this returns builds that are ordered by number of running builds
         # we prefer projects that don't use shared runners at all
-        joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+        joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
         order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
     end
 
@@ -59,7 +71,7 @@ module Ci
 
     def running_builds_for_shared_runners
       Ci::Build.running.where(runner: Ci::Runner.shared).
-        group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+        group(:project_id).select(:project_id, 'count(*) AS running_builds')
     end
 
     def new_builds
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 4b47ee489cf161e01cc040a45892a8d26d3ce3bd..89da05b72bb537b0522a7680220aaae1d3199e63 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -1,17 +1,9 @@
 module Ci
   class RetryBuildService < ::BaseService
-    CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name
-                          allow_failure stage stage_idx trigger_request
-                          yaml_variables when environment coverage_regex]
-                            .freeze
-
-    REJECT_ATTRIBUTES = %i[id status user token coverage trace runner
-                           artifacts_file artifacts_metadata artifacts_size
-                           created_at updated_at started_at finished_at
-                           queued_at erased_by erased_at].freeze
-
-    IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url
-                           deploy job_id description].freeze
+    CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
+                         allow_failure stage stage_idx trigger_request
+                         yaml_variables when environment coverage_regex
+                         description tag_list].freeze
 
     def execute(build)
       reprocess(build).tap do |new_build|
@@ -30,7 +22,7 @@ module Ci
         raise Gitlab::Access::AccessDeniedError
       end
 
-      attributes = CLONE_ATTRIBUTES.map do |attribute|
+      attributes = CLONE_ACCESSORS.map do |attribute|
         [attribute, build.send(attribute)]
       end
 
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 2c5e130e5aaa7b61482424e3a24eecca1079c864..574561adc4ce7959169bc0504586438a8abc5185 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -1,5 +1,7 @@
 module Ci
   class RetryPipelineService < ::BaseService
+    include Gitlab::OptimisticLocking
+
     def execute(pipeline)
       unless can?(current_user, :update_pipeline, pipeline)
         raise Gitlab::Access::AccessDeniedError
@@ -12,6 +14,10 @@ module Ci
           .reprocess(build)
       end
 
+      pipeline.builds.skipped.find_each do |skipped|
+        retry_optimistic_lock(skipped) { |build| build.process }
+      end
+
       MergeRequests::AddTodoWhenBuildFailsService
         .new(project, current_user)
         .close_all(pipeline)
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 25e22f14e60b0c3d9cb3c0827cf76a6607578d33..1297a79225986ff1c794be2976803415f8dfb9a0 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -1,16 +1,16 @@
 module Commits
   class ChangeService < ::BaseService
-    class ValidationError < StandardError; end
-    class ChangeError < StandardError; end
+    ValidationError = Class.new(StandardError)
+    ChangeError = Class.new(StandardError)
 
     def execute
       @start_project = params[:start_project] || @project
       @start_branch = params[:start_branch]
       @target_branch = params[:target_branch]
       @commit = params[:commit]
-      @create_merge_request = params[:create_merge_request].present?
 
-      check_push_permissions unless @create_merge_request
+      check_push_permissions
+
       commit
     rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
            ValidationError, ChangeError => ex
@@ -26,34 +26,21 @@ module Commits
     def commit_change(action)
       raise NotImplementedError unless repository.respond_to?(action)
 
-      if @create_merge_request
-        into = @commit.public_send("#{action}_branch_name")
-        tree_branch = @start_branch
-      else
-        into = tree_branch = @target_branch
-      end
-
-      tree_id = repository.public_send(
-        "check_#{action}_content", @commit, tree_branch)
-
-      if tree_id
-        validate_target_branch(into) if @create_merge_request
+      validate_target_branch if different_branch?
 
-        repository.public_send(
-          action,
-          current_user,
-          @commit,
-          into,
-          tree_id,
-          start_project: @start_project,
-          start_branch_name: @start_branch)
+      repository.public_send(
+        action,
+        current_user,
+        @commit,
+        @target_branch,
+        start_project: @start_project,
+        start_branch_name: @start_branch)
 
-        success
-      else
-        error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
+      success
+    rescue Repository::CreateTreeError
+      error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
                      A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
-        raise ChangeError, error_msg
-      end
+      raise ChangeError, error_msg
     end
 
     def check_push_permissions
@@ -66,16 +53,17 @@ module Commits
       true
     end
 
-    def validate_target_branch(new_branch)
-      # Temporary branch exists and contains the change commit
-      return if repository.find_branch(new_branch)
-
+    def validate_target_branch
       result = ValidateNewBranchService.new(@project, current_user)
-        .execute(new_branch)
+        .execute(@target_branch)
 
       if result[:status] == :error
         raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
       end
     end
+
+    def different_branch?
+      @start_branch != @target_branch || @start_project != @project
+    end
   end
 end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..297c7d696c35940efb3e4cbf375ab4192ae820b5
--- /dev/null
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -0,0 +1,32 @@
+module Issues
+  module ResolveDiscussions
+    attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
+
+    def filter_resolve_discussion_params
+      @merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
+      @discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
+    end
+
+    def merge_request_to_resolve_discussions_of
+      return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
+
+      @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id).
+                                                     execute.
+                                                     find_by(iid: merge_request_to_resolve_discussions_of_iid)
+    end
+
+    def discussions_to_resolve
+      return [] unless merge_request_to_resolve_discussions_of
+
+      @discussions_to_resolve ||=
+        if discussion_to_resolve_id
+          discussion_or_nil = merge_request_to_resolve_discussions_of
+                                .find_diff_discussion(discussion_to_resolve_id)
+          Array(discussion_or_nil)
+        else
+          merge_request_to_resolve_discussions_of
+            .resolvable_discussions
+        end
+    end
+  end
+end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 77459d8779ddf9ad3188e35de30bee21b830deaa..b07338d500ab4526e32fe587a1b87f8c09ada928 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,5 +1,7 @@
 class CreateBranchService < BaseService
   def execute(branch_name, ref)
+    create_master_branch if project.empty_repo?
+
     result = ValidateNewBranchService.new(project, current_user)
       .execute(branch_name)
 
@@ -19,4 +21,16 @@ class CreateBranchService < BaseService
   def success(branch)
     super().merge(branch: branch)
   end
+
+  private
+
+  def create_master_branch
+    project.repository.commit_file(
+      current_user,
+      '/README.md',
+      '',
+      message: 'Add README.md',
+      branch_name: 'master',
+      update: false)
+  end
 end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 0a25f56d24cb1bd7eb1a0188667e80a7c5dd3ab9..c8a60422bf487535767b814b1943bf6ba99cd0c8 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,6 +1,6 @@
 module Files
   class BaseService < ::BaseService
-    class ValidationError < StandardError; end
+    ValidationError = Class.new(StandardError)
 
     def execute
       @start_project = params[:start_project] || @project
@@ -58,16 +58,12 @@ module Files
         raise_error("You are not allowed to push into this branch")
       end
 
-      unless project.empty_repo?
-        unless @start_project.repository.branch_exists?(@start_branch)
-          raise_error('You can only create or edit files when you are on a branch')
-        end
+      if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
+        raise ValidationError, 'You can only create or edit files when you are on a branch'
+      end
 
-        if different_branch?
-          if repository.branch_exists?(@target_branch)
-            raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
-          end
-        end
+      if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
+        raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes"
       end
     end
 
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 858de5f0538abbd71f479f7a9298a5a8b5ee6556..083ffdc634cb8dc54ca5944dde1f338dc884d216 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,7 @@
 module Files
   class CreateDirService < Files::BaseService
     def commit
-      repository.commit_dir(
+      repository.create_dir(
         current_user,
         @file_path,
         message: @commit_message,
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 88dd7bbaedbfe4ab261f1dd9918cfdf7e6391c5d..65b5537fb68f9b2f780f5a793a83f413b0f04609 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,13 +1,12 @@
 module Files
   class CreateService < Files::BaseService
     def commit
-      repository.commit_file(
+      repository.create_file(
         current_user,
         @file_path,
         @file_content,
         message: @commit_message,
         branch_name: @target_branch,
-        update: false,
         author_email: @author_email,
         author_name: @author_name,
         start_project: @start_project,
@@ -17,6 +16,10 @@ module Files
     def validate
       super
 
+      if @file_content.nil?
+        raise_error("You must provide content.")
+      end
+
       if @file_path =~ Gitlab::Regex.directory_traversal_regex
         raise_error(
           'Your changes could not be committed, because the file name ' +
diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb
index c3be806a42d2b08a28de62d66ba18ef35a66f226..e294659bc98ceb6d53c7c44b5d788767ecd73759 100644
--- a/app/services/files/destroy_service.rb
+++ b/app/services/files/destroy_service.rb
@@ -1,7 +1,7 @@
 module Files
   class DestroyService < Files::BaseService
     def commit
-      repository.remove_file(
+      repository.delete_file(
         current_user,
         @file_path,
         message: @commit_message,
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index af6da5b9d56f21bb8ce1660204f73284cb806acc..700f9f4f6f0f9d86410a5e29d2c263f6abba637c 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,6 +1,8 @@
 module Files
   class MultiService < Files::BaseService
-    class FileChangedError < StandardError; end
+    FileChangedError = Class.new(StandardError)
+
+    ACTIONS = %w[create update delete move].freeze
 
     def commit
       repository.multi_action(
@@ -21,10 +23,19 @@ module Files
       super
 
       params[:actions].each_with_index do |action, index|
+        if ACTIONS.include?(action[:action].to_s)
+          action[:action] = action[:action].to_sym
+        else
+          raise_error("Unknown action type `#{action[:action]}`.")
+        end
+
         unless action[:file_path].present?
           raise_error("You must specify a file_path.")
         end
 
+        action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+        action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+
         regex_check(action[:file_path])
         regex_check(action[:previous_path]) if action[:previous_path]
 
@@ -43,8 +54,6 @@ module Files
           validate_delete(action)
         when :move
           validate_move(action, index)
-        else
-          raise_error("Unknown action type `#{action[:action]}`.")
         end
       end
     end
@@ -92,6 +101,20 @@ module Files
       if repository.blob_at_branch(params[:branch], action[:file_path])
         raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
       end
+
+      if action[:content].nil?
+        raise_error("You must provide content.")
+      end
+    end
+
+    def validate_update(action)
+      if action[:content].nil?
+        raise_error("You must provide content.")
+      end
+
+      if file_has_changed?
+        raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+      end
     end
 
     def validate_delete(action)
@@ -114,11 +137,5 @@ module Files
         params[:actions][index][:content] = blob.data
       end
     end
-
-    def validate_update(action)
-      if file_has_changed?
-        raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
-      end
-    end
   end
 end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index a71fe61a4b6a8c20e13fce5576399acf9dbdab01..fbbab97632ea0235e73c6175ff202a9f19dafb2c 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,6 +1,6 @@
 module Files
   class UpdateService < Files::BaseService
-    class FileChangedError < StandardError; end
+    FileChangedError = Class.new(StandardError)
 
     def commit
       repository.update_file(current_user, @file_path, @file_content,
@@ -18,6 +18,10 @@ module Files
     def validate
       super
 
+      if @file_content.nil?
+        raise_error("You must provide content.")
+      end
+
       if file_has_changed?
         raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
       end
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
index 27bcc0476018b216c0a8b281f39015dc2dabe6c1..ed6ea638235cb4c84f40e66383afc935d34a12ff 100644
--- a/app/services/git_operation_service.rb
+++ b/app/services/git_operation_service.rb
@@ -56,12 +56,16 @@ class GitOperationService
     start_project: repository.project,
     &block)
 
-    check_with_branch_arguments!(
-      branch_name, start_branch_name, start_project)
+    start_repository = start_project.repository
+    start_branch_name = nil if start_repository.empty_repo?
+
+    if start_branch_name && !start_repository.branch_exists?(start_branch_name)
+      raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}"
+    end
 
     update_branch_with_hooks(branch_name) do
       repository.with_repo_branch_commit(
-        start_project.repository,
+        start_repository,
         start_branch_name || branch_name,
         &block)
     end
@@ -149,31 +153,4 @@ class GitOperationService
       repository.raw_repository.autocrlf = :input
     end
   end
-
-  def check_with_branch_arguments!(
-    branch_name, start_branch_name, start_project)
-    return if repository.branch_exists?(branch_name)
-
-    if repository.project != start_project
-      unless start_branch_name
-        raise ArgumentError,
-          'Should also pass :start_branch_name if' +
-          ' :start_project is different from current project'
-      end
-
-      unless start_project.repository.branch_exists?(start_branch_name)
-        raise ArgumentError,
-          "Cannot find branch #{branch_name} nor" \
-          " #{start_branch_name} from" \
-          " #{start_project.path_with_namespace}"
-      end
-    elsif start_branch_name
-      unless repository.branch_exists?(start_branch_name)
-        raise ArgumentError,
-          "Cannot find branch #{branch_name} nor" \
-          " #{start_branch_name} from" \
-          " #{repository.project.path_with_namespace}"
-      end
-    end
-  end
 end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index dbe2fda27b5192e31165c71a23c56e2267e21e51..bc7431c89a8a631f8ce4f3952b1daffd771b7c06 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -99,6 +99,8 @@ class GitPushService < BaseService
     UpdateMergeRequestsWorker
       .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
 
+    SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks)
+
     EventCreateService.new.push(@project, current_user, build_push_data)
     @project.execute_hooks(build_push_data.dup, :push_hooks)
     @project.execute_services(build_push_data.dup, :push_hooks)
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index febeb661fb5f3bb5622a5c06591e19d223dde4df..c4e9b8fd8e0c56ab3834817fb30cdc1d72bc1811 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -2,6 +2,7 @@ module Groups
   class CreateService < Groups::BaseService
     def initialize(user, params = {})
       @current_user, @params = user, params.dup
+      @chat_team = @params.delete(:create_chat_team)
     end
 
     def execute
@@ -20,9 +21,23 @@ module Groups
       end
 
       @group.name ||= @group.path.dup
+
+      if create_chat_team?
+        response = Mattermost::CreateTeamService.new(@group, current_user).execute
+        return @group if @group.errors.any?
+
+        @group.build_chat_team(name: response['name'], team_id: response['id'])
+      end
+
       @group.save
       @group.add_owner(current_user)
       @group
     end
+
+    private
+
+    def create_chat_team?
+      Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
+    end
   end
 end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 2e2d7f884acfb8bbe5ec8b6f3f442f123114ca34..497fdb09cdc4f4f7db9a90ea791b503cb2dfd61b 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -18,7 +18,8 @@ module Groups
       end
 
       group.children.each do |group|
-        DestroyService.new(group, current_user).async_execute
+        # This needs to be synchronous since the namespace gets destroyed below
+        DestroyService.new(group, current_user).execute
       end
 
       group.really_destroy!
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 9500faf2862e8e7379dd024e9203076d2f59f2aa..b071a3984811eb97a6437eb3462f0b4e26452b46 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -203,6 +203,7 @@ class IssuableBaseService < BaseService
     change_state(issuable)
     change_subscription(issuable)
     change_todo(issuable)
+    toggle_award(issuable)
     filter_params(issuable)
     old_labels = issuable.labels.to_a
     old_mentioned_users = issuable.mentioned_users.to_a
@@ -210,7 +211,7 @@ class IssuableBaseService < BaseService
     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?
+    if issuable.changed? || params.present?
       issuable.assign_attributes(params.merge(updated_by: current_user))
 
       before_update(issuable)
@@ -263,6 +264,14 @@ class IssuableBaseService < BaseService
     end
   end
 
+  def toggle_award(issuable)
+    award = params.delete(:emoji_award)
+    if award
+      todo_service.new_award_emoji(issuable, current_user)
+      issuable.toggle_award_emoji(award, current_user)
+    end
+  end
+
   def has_changes?(issuable, old_labels: [])
     valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
 
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 35af867a09824114089bad6ffcb73188349dde77..ee1b40db718911c60ea9854b6d4028af80f7e0a9 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,13 +1,5 @@
 module Issues
   class BaseService < ::IssuableBaseService
-    attr_reader :merge_request_for_resolving_discussions
-
-    def initialize(*args)
-      super
-
-      @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
-    end
-
     def hook_data(issue, action)
       issue_data = issue.to_hook_data(current_user)
       issue_url = Gitlab::UrlBuilder.build(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7cd927d8005ca1636e50f2bbe36e2dfdaf1fb998..77bced4bd5cba327c89eed952f9c68872fefeb11 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -1,50 +1,56 @@
 module Issues
   class BuildService < Issues::BaseService
+    include ResolveDiscussions
+
     def execute
+      filter_resolve_discussion_params
       @issue = project.issues.new(issue_params)
     end
 
-    def issue_params_with_info_from_merge_request
-      return {} unless merge_request_for_resolving_discussions
+    def issue_params_with_info_from_discussions
+      return {} unless merge_request_to_resolve_discussions_of
 
-      { title: title_from_merge_request, description: description_from_merge_request }
+      { title: title_from_merge_request, description: description_for_discussions }
     end
 
     def title_from_merge_request
-      "Follow-up from \"#{merge_request_for_resolving_discussions.title}\""
+      "Follow-up from \"#{merge_request_to_resolve_discussions_of.title}\""
     end
 
-    def description_from_merge_request
-      if merge_request_for_resolving_discussions.resolvable_discussions.empty?
+    def description_for_discussions
+      if discussions_to_resolve.empty?
         return "There are no unresolved discussions. "\
-               "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}"
+               "Review the conversation in #{merge_request_to_resolve_discussions_of.to_reference}"
       end
 
-      description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:"
+      description = "The following #{'discussion'.pluralize(discussions_to_resolve.size)} "\
+                    "from #{merge_request_to_resolve_discussions_of.to_reference} "\
+                    "should be addressed:"
+
       [description, *items_for_discussions].join("\n\n")
     end
 
     def items_for_discussions
-      merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) }
+      discussions_to_resolve.map { |discussion| item_for_discussion(discussion) }
     end
 
     def item_for_discussion(discussion)
-      first_note = discussion.first_note_to_resolve
+      first_note = discussion.first_note_to_resolve || discussion.first_note
       other_note_count = discussion.notes.size - 1
-      creation_time = first_note.created_at.to_s(:medium)
       note_url = Gitlab::UrlBuilder.build(first_note)
 
-      discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): "
+      discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): "
       discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
 
       note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
-      quote = ">>>\n#{note_without_block_quotes}\n>>>"
+      spaces = ' ' * 4
+      quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join
 
       [discussion_info, quote].join("\n\n")
     end
 
     def issue_params
-      @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
+      @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
     end
 
     def whitelisted_issue_params
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 366b3572738c4a0ffbeb82633f9a9fafe3b92464..3cf4b82b9f2a29bdb5419ea2359d180a0d815d96 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,18 +1,20 @@
 module Issues
   class CreateService < Issues::BaseService
     include SpamCheckService
+    include ResolveDiscussions
 
     def execute
-      filter_spam_check_params
+      @issue = BuildService.new(project, current_user, params).execute
 
-      issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
-      @issue = BuildService.new(project, current_user, issue_attributes).execute
+      filter_spam_check_params
+      filter_resolve_discussion_params
 
       create(@issue)
     end
 
     def before_create(issue)
       spam_check(issue, current_user)
+      issue.move_to_end
     end
 
     def after_create(issuable)
@@ -20,17 +22,16 @@ module Issues
       notification_service.new_issue(issuable, current_user)
       todo_service.new_issue(issuable, current_user)
       user_agent_detail_service.create
-
-      if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
-        resolve_discussions_in_merge_request(issuable)
-      end
+      resolve_discussions_with_issue(issuable)
     end
 
-    def resolve_discussions_in_merge_request(issue)
+    def resolve_discussions_with_issue(issue)
+      return if discussions_to_resolve.empty?
+
       Discussions::ResolveService.new(project, current_user,
-                                      merge_request: merge_request_for_resolving_discussions,
+                                      merge_request: merge_request_to_resolve_discussions_of,
                                       follow_up_issue: issue).
-          execute(merge_request_for_resolving_discussions.resolvable_discussions)
+        execute(discussions_to_resolve)
     end
 
     private
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index a2a5f57d069db4907b1d34fb41b3f61b5106363a..711f4035c55a4ae6617c1b1d0a102c7c624e61de 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -1,6 +1,6 @@
 module Issues
   class MoveService < Issues::BaseService
-    class MoveError < StandardError; end
+    MoveError = Class.new(StandardError)
 
     def execute(issue, new_project)
       @old_issue = issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 22e32b1325943ae796d1605c059aefed3c6c3668..a444c78b609cfd965a7713a767a169aa40a6f30d 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -3,8 +3,8 @@ module Issues
     include SpamCheckService
 
     def execute(issue)
+      handle_move_between_iids(issue)
       filter_spam_check_params
-
       update(issue)
     end
 
@@ -37,11 +37,13 @@ module Issues
       end
 
       added_labels = issue.labels - old_labels
+
       if added_labels.present?
         notification_service.relabeled_issue(issue, added_labels, current_user)
       end
 
       added_mentions = issue.mentioned_users - old_mentioned_users
+
       if added_mentions.present?
         notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
       end
@@ -55,8 +57,24 @@ module Issues
       Issues::CloseService
     end
 
+    def handle_move_between_iids(issue)
+      return unless params[:move_between_iids]
+
+      after_iid, before_iid = params.delete(:move_between_iids)
+
+      issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
+      issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
+
+      issue.move_between(issue_before, issue_after)
+    end
+
     private
 
+    def get_issue_if_allowed(project, iid)
+      issue = project.issues.find_by(iid: iid)
+      issue if can?(current_user, :update_issue, issue)
+    end
+
     def create_confidentiality_note(issue)
       SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
     end
diff --git a/app/services/mattermost/create_team_service.rb b/app/services/mattermost/create_team_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e3206810f3a84969adf27843d8cd48e971f861da
--- /dev/null
+++ b/app/services/mattermost/create_team_service.rb
@@ -0,0 +1,14 @@
+module Mattermost
+  class CreateTeamService < ::BaseService
+    def initialize(group, current_user)
+      @group, @current_user = group, current_user
+    end
+
+    def execute
+      # The user that creates the team will be Team Admin
+      Mattermost::Team.new(current_user).create(@group.mattermost_team_params)
+    rescue Mattermost::ClientError => e
+      @group.errors.add(:mattermost_team, e.message)
+    end
+  end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 431da8372c96d0403ecf7c562999daa65dc9c524..2e089149ca885e2e754188b840d991fc8945397a 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -4,7 +4,7 @@ module Members
 
     attr_accessor :source
 
-    ALLOWED_SCOPES = %i[members requesters all]
+    ALLOWED_SCOPES = %i[members requesters all].freeze
 
     def initialize(source, current_user, params = {})
       @source = source
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bba66901e4d6d09cc0bb555c80670d1..fdce542bd9e04080379ef2c5e338c66a56ff5efe 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
       merge_request.source_project  = find_source_project
       merge_request.target_project  = find_target_project
       merge_request.target_branch   = find_target_branch
-      merge_request.can_be_created  = branches_valid? && source_branch_specified? && target_branch_specified?
+      merge_request.can_be_created  = branches_valid?
 
       compare_branches if branches_present?
       assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 1262ecbc29aa00a9ace6ac63244ba2846e7ee693..f00a33969a8bb2914aaaa5776572972195ab64b7 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
     end
 
     def execute(changes)
+      return [] unless project.printing_merge_request_link_enabled
+
       branches = get_branches(changes)
       merge_requests_map = opened_merge_requests_from_source_branches(branches)
       branches.map do |branch|
@@ -23,10 +25,7 @@ module MergeRequests
 
     def opened_merge_requests_from_source_branches(branches)
       merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
-      merge_requests.inject({}) do |hash, mr|
-        hash[mr.source_branch] = mr
-        hash
-      end
+      merge_requests.index_by(&:source_branch)
     end
 
     def get_branches(changes)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 3da1b657a41723fd04c513fa6204db5f2123fc6a..fac3ac7a4c70d1ae08973a7181950acaf1f6ac31 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,6 +6,8 @@ module MergeRequests
   # Executed when you do merge via GitLab UI
   #
   class MergeService < MergeRequests::BaseService
+    MergeError = Class.new(StandardError)
+
     attr_reader :merge_request, :source
 
     def execute(merge_request)
@@ -27,6 +29,8 @@ module MergeRequests
           success
         end
       end
+    rescue MergeError => e
+      log_merge_error(e.message, save_message_on_model: true)
     end
 
     private
@@ -42,19 +46,13 @@ module MergeRequests
 
       commit_id = repository.merge(current_user, source, merge_request, options)
 
-      if commit_id
-        merge_request.update(merge_commit_sha: commit_id)
-      else
-        log_merge_error('Conflicts detected during merge', save_message_on_model: true)
-        false
-      end
+      raise MergeError, 'Conflicts detected during merge' unless commit_id
+
+      merge_request.update(merge_commit_sha: commit_id)
     rescue GitHooksService::PreReceiveError => e
-      log_merge_error(e.message, save_message_on_model: true)
-      false
+      raise MergeError, e.message
     rescue StandardError => e
-      merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
-      log_merge_error(e.message)
-      false
+      raise MergeError, "Something went wrong during merge: #{e.message}"
     ensure
       merge_request.update(in_progress_merge_commit_sha: nil)
     end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index 5616edf8b4a303db8b65cf37dc59ded8a1270e2a..aed5287940ee0ad68023b136456690b0483fc6fa 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -1,18 +1,18 @@
 module MergeRequests
   class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
-    # Marks the passed `merge_request` to be merged when the build succeeds or
+    # Marks the passed `merge_request` to be merged when the pipeline succeeds or
     # updates the params for the automatic merge
     def execute(merge_request)
       merge_request.merge_params.merge!(params)
 
       # The service is also called when the merge params are updated.
-      already_approved = merge_request.merge_when_build_succeeds?
+      already_approved = merge_request.merge_when_pipeline_succeeds?
 
       unless already_approved
-        merge_request.merge_when_build_succeeds = true
-        merge_request.merge_user                = @current_user
+        merge_request.merge_when_pipeline_succeeds = true
+        merge_request.merge_user = @current_user
 
-        SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
+        SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
       end
 
       merge_request.save
@@ -23,8 +23,12 @@ module MergeRequests
       return unless pipeline.success?
 
       pipeline_merge_requests(pipeline) do |merge_request|
-        next unless merge_request.merge_when_build_succeeds?
-        next unless merge_request.mergeable?
+        next unless merge_request.merge_when_pipeline_succeeds?
+
+        unless merge_request.mergeable?
+          todo_service.merge_request_became_unmergeable(merge_request)
+          next
+        end
 
         MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
       end
@@ -32,9 +36,9 @@ module MergeRequests
 
     # Cancels the automatic merge
     def cancel(merge_request)
-      if merge_request.merge_when_build_succeeds? && merge_request.open?
-        merge_request.reset_merge_when_build_succeeds
-        SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
+      if merge_request.merge_when_pipeline_succeeds? && merge_request.open?
+        merge_request.reset_merge_when_pipeline_succeeds
+        SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
 
         success
       else
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 581d18032e6802ad0bb75a81fb05eb1ae6a23761..1131d6f4913bb3c4ba291319dd43540f320e428d 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -11,7 +11,7 @@ module MergeRequests
       # empty diff during a manual merge
       close_merge_requests
       reload_merge_requests
-      reset_merge_when_build_succeeds
+      reset_merge_when_pipeline_succeeds
       mark_pending_todos_done
       cache_merge_requests_closing_issues
 
@@ -78,8 +78,8 @@ module MergeRequests
       end
     end
 
-    def reset_merge_when_build_succeeds
-      merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
+    def reset_merge_when_pipeline_succeeds
+      merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
     end
 
     def mark_pending_todos_done
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index d22a1d3e0ad3d2c2f797080c41368d4d70074da9..82cd89d9a0bdf37f8f0ded33eabe88f18b6ff344 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,7 +1,6 @@
 module MergeRequests
   class ResolveService < MergeRequests::BaseService
-    class MissingFiles < Gitlab::Conflict::ResolutionError
-    end
+    MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
 
     attr_accessor :conflicts, :rugged, :merge_index, :merge_request
 
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index b4f8b33d564b40e3ae3c6b4813c73407b367713b..61d66a26932bc41d7af3e96931be03fad336f6d4 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,14 +8,6 @@ module Notes
       note.author  = current_user
       note.system  = false
 
-      if note.award_emoji?
-        noteable = note.noteable
-        if noteable.user_can_award?(current_user, note.award_emoji_name)
-          todo_service.new_award_emoji(noteable, current_user)
-          return noteable.create_award_emoji(note.award_emoji_name, current_user)
-        end
-      end
-
       # We execute commands (extracted from `params[:note]`) on the noteable
       # **before** we save the note because if the note consists of commands
       # only, there is no need be create a note!
@@ -48,7 +40,7 @@ module Notes
           note.errors.add(:commands_only, 'Commands applied')
         end
 
-        note.commands_changes = command_params.keys
+        note.commands_changes = command_params
       end
 
       note
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 56913568cae70e250caa8eb5269fd101ca46f21c..ad1e6f6774a174c6b84d28488747d801094ccc67 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -3,7 +3,7 @@ module Notes
     UPDATE_SERVICES = {
       'Issue' => Issues::UpdateService,
       'MergeRequest' => MergeRequests::UpdateService
-    }
+    }.freeze
 
     def self.noteable_update_service(note)
       UPDATE_SERVICES[note.noteable_type]
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44ae23fad18109807352daa045278710a875f5db
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+  attr_reader :project
+  
+  def initialize(project)
+    @project = project
+  end
+
+  def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+    custom_action = build_custom_key(action, target)
+
+    recipients = target.participants(current_user)
+
+    unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+      recipients = add_project_watchers(recipients)
+    end
+
+    recipients = add_custom_notifications(recipients, custom_action)
+    recipients = reject_mention_users(recipients)
+
+    # Re-assign is considered as a mention of the new assignee so we add the
+    # new assignee to the list of recipients after we rejected users with
+    # the "on mention" notification level
+    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+      recipients << previous_assignee if previous_assignee
+      recipients << target.assignee
+    end
+
+    recipients = reject_muted_users(recipients)
+    recipients = add_subscribed_users(recipients, target)
+
+    if [:new_issue, :new_merge_request].include?(custom_action)
+      recipients = add_labels_subscribers(recipients, target)
+    end
+
+    recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
+
+    recipients.delete(current_user) if skip_current_user
+
+    recipients.uniq
+  end
+
+  def build_relabeled_recipients(target, current_user, labels:)
+    recipients = add_labels_subscribers([], target, labels: labels)
+    recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
+    recipients.delete(current_user)
+    recipients.uniq
+  end
+
+  def build_new_note_recipients(note)
+    target = note.noteable
+
+    ability, subject = if note.for_personal_snippet?
+                         [:read_personal_snippet, note.noteable]
+                       else
+                         [:read_project, note.project]
+                       end
+
+    mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+    # Add all users participating in the thread (author, assignee, comment authors)
+    recipients =
+      if target.respond_to?(:participants)
+        target.participants(note.author)
+      else
+        mentioned_users
+      end
+
+    unless note.for_personal_snippet?
+      # Merge project watchers
+      recipients = add_project_watchers(recipients)
+
+      # Merge project with custom notification
+      recipients = add_custom_notifications(recipients, :new_note)
+    end
+
+    # Reject users with Mention notification level, except those mentioned in _this_ note.
+    recipients = reject_mention_users(recipients - mentioned_users)
+    recipients = recipients + mentioned_users
+
+    recipients = reject_muted_users(recipients)
+
+    recipients = add_subscribed_users(recipients, note.noteable)
+    recipients = reject_unsubscribed_users(recipients, note.noteable)
+    recipients = reject_users_without_access(recipients, note.noteable)
+
+    recipients.delete(note.author)
+    recipients.uniq
+  end
+
+  # Remove users with disabled notifications from array
+  # Also remove duplications and nil recipients
+  def reject_muted_users(users)
+    reject_users(users, :disabled)
+  end
+
+  protected
+
+  # Get project/group users with CUSTOM notification level
+  def add_custom_notifications(recipients, action)
+    user_ids = []
+
+    # Users with a notification setting on group or project
+    user_ids += user_ids_notifiable_on(project, :custom, action)
+    user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+    # Users with global level custom
+    user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+    user_ids_with_group_level_global   = user_ids_notifiable_on(project.group, :global)
+
+    global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+    user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+    recipients.concat(User.find(user_ids))
+  end
+
+  def add_project_watchers(recipients)
+    recipients.concat(project_watchers).compact
+  end
+
+  # Get project users with WATCH notification level
+  def project_watchers
+    project_members_ids = user_ids_notifiable_on(project)
+
+    user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+    user_ids_with_group_global   = user_ids_notifiable_on(project.group, :global)
+
+    user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+    user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+    user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+    User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+  end
+
+  # Remove users with notification level 'Mentioned'
+  def reject_mention_users(users)
+    reject_users(users, :mention)
+  end
+
+  def add_subscribed_users(recipients, target)
+    return recipients unless target.respond_to? :subscribers
+
+    recipients + target.subscribers(project)
+  end
+
+  def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+    return [] unless resource
+
+    if notification_level
+      settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+      settings = settings.select { |setting| setting.events[action] } if action.present?
+      settings.map(&:user_id)
+    else
+      resource.notification_settings.pluck(:user_id)
+    end
+  end
+
+  # Build a list of user_ids based on project notification settings
+  def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+    user_ids = user_ids_notifiable_on(project, :watch)
+
+    # If project setting is global, add to watch list if global setting is watch
+    global_setting.each do |user_id|
+      if user_ids_global_level_watch.include?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    user_ids
+  end
+
+  # Build a list of user_ids based on group notification settings
+  def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+    uids = user_ids_notifiable_on(group, :watch)
+
+    # Group setting is watch, add to user_ids list if user is not project member
+    user_ids = []
+    uids.each do |user_id|
+      if project_members.exclude?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    # Group setting is global, add to user_ids list if global setting is watch
+    global_setting.each do |user_id|
+      if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    user_ids
+  end
+
+  def user_ids_with_global_level_watch(ids)
+    settings_with_global_level_of(:watch, ids).pluck(:user_id)
+  end
+
+  def user_ids_with_global_level_custom(ids, action)
+    settings = settings_with_global_level_of(:custom, ids)
+    settings = settings.select { |setting| setting.events[action] }
+    settings.map(&:user_id)
+  end
+
+  def settings_with_global_level_of(level, ids)
+    NotificationSetting.where(
+      user_id: ids,
+      source_type: nil,
+      level: NotificationSetting.levels[level]
+    )
+  end
+
+  # Reject users which has certain notification level
+  #
+  # Example:
+  #   reject_users(users, :watch, project)
+  #
+  def reject_users(users, level)
+    level = level.to_s
+
+    unless NotificationSetting.levels.keys.include?(level)
+      raise 'Invalid notification level'
+    end
+
+    users = users.to_a.compact.uniq
+    users = users.select { |u| u.can?(:receive_notifications) }
+
+    users.reject do |user|
+      global_notification_setting = user.global_notification_setting
+
+      next global_notification_setting.level == level unless project
+
+      setting = user.notification_settings_for(project)
+
+      if project.group && (setting.nil? || setting.global?)
+        setting = user.notification_settings_for(project.group)
+      end
+
+      # reject users who globally set mention notification and has no setting per project/group
+      next global_notification_setting.level == level unless setting
+
+      # reject users who set mention notification in project
+      next true if setting.level == level
+
+      # reject users who have mention level in project and disabled in global settings
+      setting.global? && global_notification_setting.level == level
+    end
+  end
+
+  def reject_unsubscribed_users(recipients, target)
+    return recipients unless target.respond_to? :subscriptions
+
+    recipients.reject do |user|
+      subscription = target.subscriptions.find_by_user_id(user.id)
+      subscription && !subscription.subscribed
+    end
+  end
+
+  def reject_users_without_access(recipients, target)
+    ability = case target
+              when Issuable
+                :"read_#{target.to_ability_name}"
+              when Ci::Pipeline
+                :read_build # We have build trace in pipeline emails
+              end
+
+    return recipients unless ability
+
+    recipients.select do |user|
+      user.can?(ability, target)
+    end
+  end
+
+  def add_labels_subscribers(recipients, target, labels: nil)
+    return recipients unless target.respond_to? :labels
+
+    (labels || target.labels).each do |label|
+      recipients += label.subscribers(project)
+    end
+
+    recipients
+  end
+
+  # Build event key to search on custom notification level
+  # Check NotificationSetting::EMAIL_EVENTS
+  def build_custom_key(action, object)
+    "#{action}_#{object.class.model_name.name.underscore}".to_sym
+  end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3734e3c4253cc0e7e83517990ddc317d726f14ba..f9aa234675982a651e4c3d78bf4f1c04a7eebde6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -135,7 +135,7 @@ class NotificationService
       merge_request.target_project,
       current_user,
       :merged_merge_request_email,
-      skip_current_user: !merge_request.merge_when_build_succeeds?
+      skip_current_user: !merge_request.merge_when_pipeline_succeeds?
     )
   end
 
@@ -150,7 +150,10 @@ class NotificationService
   end
 
   def resolve_all_discussions(merge_request, current_user)
-    recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+    recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+      merge_request,
+      current_user,
+      action: "resolve_all_discussions")
 
     recipients.each do |recipient|
       mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
   end
 
   # Notify users on new note in system
-  #
-  # TODO: split on methods and refactor
-  #
   def new_note(note)
     return true unless note.noteable_type.present?
 
     # ignore gitlab service messages
     return true if note.cross_reference? && note.system?
 
-    target = note.noteable
-
-    recipients = []
-
-    mentioned_users = note.mentioned_users
-
-    ability, subject = if note.for_personal_snippet?
-                         [:read_personal_snippet, note.noteable]
-                       else
-                         [:read_project, note.project]
-                       end
-
-    mentioned_users.select! do |user|
-      user.can?(ability, subject)
-    end
-
-    # Add all users participating in the thread (author, assignee, comment authors)
-    participants =
-      if target.respond_to?(:participants)
-        target.participants(note.author)
-      else
-        mentioned_users
-      end
-
-    recipients = recipients.concat(participants)
-
-    unless note.for_personal_snippet?
-      # Merge project watchers
-      recipients = add_project_watchers(recipients, note.project)
-
-      # Merge project with custom notification
-      recipients = add_custom_notifications(recipients, note.project, :new_note)
-    end
-
-    # Reject users with Mention notification level, except those mentioned in _this_ note.
-    recipients = reject_mention_users(recipients - mentioned_users, note.project)
-    recipients = recipients + mentioned_users
-
-    recipients = reject_muted_users(recipients, note.project)
-
-    recipients = add_subscribed_users(recipients, note.project, note.noteable)
-    recipients = reject_unsubscribed_users(recipients, note.noteable)
-    recipients = reject_users_without_access(recipients, note.noteable)
-
-    recipients.delete(note.author) unless note.author.notified_of_own_activity?
-    recipients = recipients.uniq
-
     notify_method = "note_#{note.to_ability_name}_email".to_sym
 
+    recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
     recipients.each do |recipient|
       mailer.send(notify_method, recipient.id, note.id).deliver_later
     end
@@ -290,7 +244,7 @@ class NotificationService
 
   def project_was_moved(project, old_path_with_namespace)
     recipients = project.team.members
-    recipients = reject_muted_users(recipients, project)
+    recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
 
     recipients.each do |recipient|
       mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
   end
 
   def issue_moved(issue, new_issue, current_user)
-    recipients = build_recipients(issue, issue.project, current_user)
+    recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
 
     recipients.map do |recipient|
       email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,12 +278,10 @@ class NotificationService
 
     return unless mailer.respond_to?(email_template)
 
-    recipients ||= build_recipients(
+    recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
       pipeline,
-      pipeline.project,
-      pipeline.user,
-      action: pipeline.status,
-      skip_current_user: false).map(&:notification_email)
+      nil, # The acting user, who won't be added to recipients
+      action: pipeline.status).map(&:notification_email)
 
     if recipients.any?
       mailer.public_send(email_template, pipeline, recipients).deliver_later
@@ -338,199 +290,8 @@ class NotificationService
 
   protected
 
-  # Get project/group users with CUSTOM notification level
-  def add_custom_notifications(recipients, project, action)
-    user_ids = []
-
-    # Users with a notification setting on group or project
-    user_ids += notification_settings_for(project, :custom, action)
-    user_ids += notification_settings_for(project.group, :custom, action)
-
-    # Users with global level custom
-    users_with_project_level_global = notification_settings_for(project, :global)
-    users_with_group_level_global   = notification_settings_for(project.group, :global)
-
-    global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
-    user_ids += users_with_global_level_custom(global_users_ids, action)
-
-    recipients.concat(User.find(user_ids))
-  end
-
-  # Get project users with WATCH notification level
-  def project_watchers(project)
-    project_members = notification_settings_for(project)
-
-    users_with_project_level_global = notification_settings_for(project, :global)
-    users_with_group_level_global   = notification_settings_for(project.group, :global)
-
-    users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
-    users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
-    users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
-    User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
-  end
-
-  def notification_settings_for(resource, notification_level = nil, action = nil)
-    return [] unless resource
-
-    if notification_level
-      settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
-      settings = settings.select { |setting| setting.events[action] } if action.present?
-      settings.map(&:user_id)
-    else
-      resource.notification_settings.pluck(:user_id)
-    end
-  end
-
-  def users_with_global_level_watch(ids)
-    settings_with_global_level_of(:watch, ids).pluck(:user_id)
-  end
-
-  def users_with_global_level_custom(ids, action)
-    settings = settings_with_global_level_of(:custom, ids)
-    settings = settings.select { |setting| setting.events[action] }
-    settings.map(&:user_id)
-  end
-
-  def settings_with_global_level_of(level, ids)
-    NotificationSetting.where(
-      user_id: ids,
-      source_type: nil,
-      level: NotificationSetting.levels[level]
-    )
-  end
-
-  # Build a list of users based on project notification settings
-  def select_project_member_setting(project, global_setting, users_global_level_watch)
-    users = notification_settings_for(project, :watch)
-
-    # If project setting is global, add to watch list if global setting is watch
-    global_setting.each do |user_id|
-      if users_global_level_watch.include?(user_id)
-        users << user_id
-      end
-    end
-
-    users
-  end
-
-  # Build a list of users based on group notification settings
-  def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
-    uids = notification_settings_for(group, :watch)
-
-    # Group setting is watch, add to users list if user is not project member
-    users = []
-    uids.each do |user_id|
-      if project_members.exclude?(user_id)
-        users << user_id
-      end
-    end
-
-    # Group setting is global, add to users list if global setting is watch
-    global_setting.each do |user_id|
-      if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
-        users << user_id
-      end
-    end
-
-    users
-  end
-
-  def add_project_watchers(recipients, project)
-    recipients.concat(project_watchers(project)).compact
-  end
-
-  # Remove users with disabled notifications from array
-  # Also remove duplications and nil recipients
-  def reject_muted_users(users, project = nil)
-    reject_users(users, :disabled, project)
-  end
-
-  # Remove users with notification level 'Mentioned'
-  def reject_mention_users(users, project = nil)
-    reject_users(users, :mention, project)
-  end
-
-  # Reject users which has certain notification level
-  #
-  # Example:
-  #   reject_users(users, :watch, project)
-  #
-  def reject_users(users, level, project = nil)
-    level = level.to_s
-
-    unless NotificationSetting.levels.keys.include?(level)
-      raise 'Invalid notification level'
-    end
-
-    users = users.to_a.compact.uniq
-    users = users.reject(&:blocked?)
-
-    users.reject do |user|
-      global_notification_setting = user.global_notification_setting
-
-      next global_notification_setting.level == level unless project
-
-      setting = user.notification_settings_for(project)
-
-      if project.group && (setting.nil? || setting.global?)
-        setting = user.notification_settings_for(project.group)
-      end
-
-      # reject users who globally set mention notification and has no setting per project/group
-      next global_notification_setting.level == level unless setting
-
-      # reject users who set mention notification in project
-      next true if setting.level == level
-
-      # reject users who have mention level in project and disabled in global settings
-      setting.global? && global_notification_setting.level == level
-    end
-  end
-
-  def reject_unsubscribed_users(recipients, target)
-    return recipients unless target.respond_to? :subscriptions
-
-    recipients.reject do |user|
-      subscription = target.subscriptions.find_by_user_id(user.id)
-      subscription && !subscription.subscribed
-    end
-  end
-
-  def reject_users_without_access(recipients, target)
-    ability = case target
-              when Issuable
-                :"read_#{target.to_ability_name}"
-              when Ci::Pipeline
-                :read_build # We have build trace in pipeline emails
-              end
-
-    return recipients unless ability
-
-    recipients.select do |user|
-      user.can?(ability, target)
-    end
-  end
-
-  def add_subscribed_users(recipients, project, target)
-    return recipients unless target.respond_to? :subscribers
-
-    recipients + target.subscribers(project)
-  end
-
-  def add_labels_subscribers(recipients, project, target, labels: nil)
-    return recipients unless target.respond_to? :labels
-
-    (labels || target.labels).each do |label|
-      recipients += label.subscribers(project)
-    end
-
-    recipients
-  end
-
   def new_resource_email(target, project, method)
-    recipients = build_recipients(target, project, target.author, action: "new")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
 
     recipients.each do |recipient|
       mailer.send(method, recipient.id, target.id).deliver_later
@@ -538,7 +299,7 @@ class NotificationService
   end
 
   def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
-    recipients = build_recipients(target, project, current_user, action: "new")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
     recipients = recipients & new_mentioned_users
 
     recipients.each do |recipient|
@@ -549,9 +310,8 @@ class NotificationService
   def close_resource_email(target, project, current_user, method, skip_current_user: true)
     action = method == :merged_merge_request_email ? "merge" : "close"
 
-    recipients = build_recipients(
+    recipients = NotificationRecipientService.new(project).build_recipients(
       target,
-      project,
       current_user,
       action: action,
       skip_current_user: skip_current_user
@@ -566,7 +326,12 @@ class NotificationService
     previous_assignee_id = previous_record(target, 'assignee_id')
     previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
 
-    recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+    recipients = NotificationRecipientService.new(project).build_recipients(
+      target,
+      current_user,
+      action: "reassign",
+      previous_assignee: previous_assignee
+    )
 
     recipients.each do |recipient|
       mailer.send(
@@ -580,7 +345,7 @@ class NotificationService
   end
 
   def relabeled_resource_email(target, project, labels, current_user, method)
-    recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+    recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
     label_names = labels.map(&:name)
 
     recipients.each do |recipient|
@@ -589,58 +354,13 @@ class NotificationService
   end
 
   def reopen_resource_email(target, project, current_user, method, status)
-    recipients = build_recipients(target, project, current_user, action: "reopen")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
 
     recipients.each do |recipient|
       mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
     end
   end
 
-  def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
-    custom_action = build_custom_key(action, target)
-
-    recipients = target.participants(current_user)
-
-    unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
-      recipients = add_project_watchers(recipients, project)
-    end
-
-    recipients = add_custom_notifications(recipients, project, custom_action)
-    recipients = reject_mention_users(recipients, project)
-
-    recipients = recipients.uniq
-
-    # Re-assign is considered as a mention of the new assignee so we add the
-    # new assignee to the list of recipients after we rejected users with
-    # the "on mention" notification level
-    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
-      recipients << previous_assignee if previous_assignee
-      recipients << target.assignee
-    end
-
-    recipients = reject_muted_users(recipients, project)
-    recipients = add_subscribed_users(recipients, project, target)
-
-    if [:new_issue, :new_merge_request].include?(custom_action)
-      recipients = add_labels_subscribers(recipients, project, target)
-    end
-
-    recipients = reject_unsubscribed_users(recipients, target)
-    recipients = reject_users_without_access(recipients, target)
-
-    recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
-
-    recipients.uniq
-  end
-
-  def build_relabeled_recipients(target, project, current_user, labels:)
-    recipients = add_labels_subscribers([], project, target, labels: labels)
-    recipients = reject_unsubscribed_users(recipients, target)
-    recipients = reject_users_without_access(recipients, target)
-    recipients.delete(current_user) unless current_user.notified_of_own_activity?
-    recipients.uniq
-  end
-
   def mailer
     Notify
   end
@@ -652,10 +372,4 @@ class NotificationService
       end
     end
   end
-
-  # Build event key to search on custom notification level
-  # Check NotificationSetting::EMAIL_EVENTS
-  def build_custom_key(action, object)
-    "#{action}_#{object.class.model_name.name.underscore}".to_sym
-  end
 end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 6dc3d8c2416012723c49758d4c53ea09e31765f1..fbdaa45565104fbcf1d81f141428e6d79703ceec 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -12,7 +12,7 @@ module Projects
       @project = Project.new(params)
 
       # Make sure that the user is allowed to use the specified visibility level
-      unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+      unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level)
         deny_visibility_level(@project)
         return @project
       end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index ba410b79e8c0750ef0b888757ca0f134e43f5eab..4e1964f79ddd6cad88498b54193692e6a0c13c61 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -2,9 +2,9 @@ module Projects
   class DestroyService < BaseService
     include Gitlab::ShellAdapter
 
-    class DestroyError < StandardError; end
+    DestroyError = Class.new(StandardError)
 
-    DELETED_FLAG = '+deleted'
+    DELETED_FLAG = '+deleted'.freeze
 
     def async_execute
       project.transaction do
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index f06a3d44c175e41b2e31a52f0003e83ebe4e5038..604747e39d0ad076634a1d76f0709a76a0fbc833 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -2,7 +2,7 @@ module Projects
   class DownloadService < BaseService
     WHITELIST = [
       /^[^.]+\.fogbugz.com$/
-    ]
+    ].freeze
 
     def initialize(project, url)
       @project, @url = project, url
@@ -25,7 +25,7 @@ module Projects
     end
 
     def http?(url)
-      url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+      url =~ /\A#{URI.regexp(%w(http https))}\z/
     end
 
     def valid_domain?(url)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cd230528743f9bcdef5642afa05e32c7c68e06bc..d484a96f785d3b6d888b0e9655e350ec7b80535c 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -2,7 +2,7 @@ module Projects
   class ImportService < BaseService
     include Gitlab::ShellAdapter
 
-    class Error < StandardError; end
+    Error = Class.new(StandardError)
 
     def execute
       add_repository_to_project unless project.gitlab_project_import?
@@ -33,6 +33,7 @@ module Projects
 
     def import_repository
       begin
+        raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
         gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
       rescue => e
         # Expire cache to prevent scenarios such as:
@@ -40,7 +41,7 @@ module Projects
         # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
         project.repository.before_import if project.repository_exists?
 
-        raise Error,  "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+        raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
       end
     end
 
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 3e241b9e7c05594878da96c279231661ea4ca748..6d9e7de4f2473891c39d343ada31ad3b433101fa 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -9,7 +9,7 @@
 module Projects
   class TransferService < BaseService
     include Gitlab::ShellAdapter
-    class TransferError < StandardError; end
+    TransferError = Class.new(StandardError)
 
     def execute(new_namespace)
       if allowed_transfer?(current_user, project, new_namespace)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index f5f9ee88912841d19219242d9aaca0cc6784bf2d..523b9f4191642fb7737239fc9f8a3b3b02bd9655 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -2,7 +2,7 @@ module Projects
   class UpdatePagesService < BaseService
     BLOCK_SIZE = 32.kilobytes
     MAX_SIZE = 1.terabyte
-    SITE_PATH = 'public/'
+    SITE_PATH = 'public/'.freeze
 
     attr_reader :build
 
@@ -34,6 +34,8 @@ module Projects
       end
     rescue => e
       error(e.message)
+    ensure
+      build.erase_artifacts! unless build.has_expiring_artifacts?
     end
 
     private
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
index 050cb3b738b6cc92993cd4ddafe0607391bff69a..bdb0e0cc8bf2cc4cee732e84c93764fcd94029ed 100644
--- a/app/services/protected_branches/api_update_service.rb
+++ b/app/services/protected_branches/api_update_service.rb
@@ -15,16 +15,16 @@ module ProtectedBranches
 
         case @developers_can_push
         when true
-          params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+          params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
         when false
-          params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+          params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
         end
 
         case @developers_can_merge
         when true
-          params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+          params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
         when false
-          params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+          params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
         end
 
         service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 3e0a85cf05953ff5a709150876002669a6417fba..595653ea58a5b91f8138143553a5821c7da09987 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -59,7 +59,7 @@ module SlashCommands
       @updates[:state_event] = 'reopen'
     end
 
-    desc 'Merge (when build succeeds)'
+    desc 'Merge (when the pipeline succeeds)'
     condition do
       last_diff_sha = params && params[:merge_request_diff_head_sha]
       issuable.is_a?(MergeRequest) &&
@@ -255,6 +255,18 @@ module SlashCommands
       @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
     end
 
+    desc 'Toggle emoji reward'
+    params ':emoji:'
+    condition do
+      issuable.persisted?
+    end
+    command :award do |emoji|
+      name = award_emoji_name(emoji)
+      if name && issuable.user_can_award?(current_user, name)
+        @updates[:emoji_award] = name
+      end
+    end
+
     desc 'Set time estimate'
     params '<1w 3d 2h 14m>'
     condition do
@@ -329,5 +341,10 @@ module SlashCommands
 
       ext.references(type)
     end
+
+    def award_emoji_name(emoji)
+      match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)
+      match[1] if match
+    end
   end
 end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a2bfa422c9d4572f2becdd5211119518fd7588d4..af0ddbe59349a7aaa3428c961bf74fbf2851ba7e 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -24,21 +24,16 @@ class SystemHooksService
         key: model.key,
         id: model.id
       )
+ 
       if model.user
-        data.merge!(
-          username: model.user.username
-        )
+        data[:username] = model.user.username
       end
     when Project
       data.merge!(project_data(model))
 
       if event == :rename || event == :transfer
-        data.merge!({
-          old_path_with_namespace: model.old_path_with_namespace
-        })
+        data[:old_path_with_namespace] = model.old_path_with_namespace
       end
-
-      data
     when User
       data.merge!({
         name: model.name,
@@ -61,6 +56,8 @@ class SystemHooksService
     when GroupMember
       data.merge!(group_member_data(model))
     end
+    
+    data
   end
 
   def build_event_name(model, event)
@@ -86,7 +83,7 @@ class SystemHooksService
       project_id: model.id,
       owner_name: owner.name,
       owner_email: owner.respond_to?(:email) ? owner.email : "",
-      project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
+      project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase
     }
   end
 
@@ -103,7 +100,7 @@ class SystemHooksService
       user_email:                   model.user.email,
       user_id:                      model.user.id,
       access_level:                 model.human_access,
-      project_visibility:           Project.visibility_levels.key(project.visibility_level_field).downcase
+      project_visibility:           Project.visibility_levels.key(project.visibility_level_value).downcase
     }
   end
 
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 87ba72cf99190aeb9fd7f6b00c45a59308b23aca..8e02fe3741a2e095e34d55fa8c7d745c06675cce 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -187,14 +187,14 @@ module SystemNoteService
   end
 
   # Called when 'merge when pipeline succeeds' is executed
-  def merge_when_build_succeeds(noteable, project, author, last_commit)
+  def merge_when_pipeline_succeeds(noteable, project, author, last_commit)
     body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
 
     create_note(noteable: noteable, project: project, author: author, note: body)
   end
 
   # Called when 'merge when pipeline succeeds' is canceled
-  def cancel_merge_when_build_succeeds(noteable, project, author)
+  def cancel_merge_when_pipeline_succeeds(noteable, project, author)
     body = 'canceled the automatic merge'
 
     create_note(noteable: noteable, project: project, author: author, note: body)
@@ -356,10 +356,10 @@ module SystemNoteService
       note:    cross_reference_note_content(gfm_reference)
     }
 
-    if noteable.kind_of?(Commit)
+    if noteable.is_a?(Commit)
       note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
     else
-      note_options.merge!(noteable: noteable)
+      note_options[:noteable] = noteable
     end
 
     if noteable.is_a?(ExternalIssue)
@@ -385,7 +385,6 @@ module SystemNoteService
   # Returns Boolean
   def cross_reference_disallowed?(noteable, mentioner)
     return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
-    return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
     return false unless mentioner.is_a?(MergeRequest)
     return false unless noteable.is_a?(Commit)
 
@@ -408,12 +407,13 @@ module SystemNoteService
     # Initial scope should be system notes of this noteable type
     notes = Note.system.where(noteable_type: noteable.class)
 
-    if noteable.is_a?(Commit)
-      # Commits have non-integer IDs, so they're stored in `commit_id`
-      notes = notes.where(commit_id: noteable.id)
-    else
-      notes = notes.where(noteable_id: noteable.id)
-    end
+    notes =
+      if noteable.is_a?(Commit)
+        # Commits have non-integer IDs, so they're stored in `commit_id`
+        notes.where(commit_id: noteable.id)
+      else
+        notes.where(noteable_id: noteable.id)
+      end
 
     notes_for_mentioner(mentioner, noteable, notes).exists?
   end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 910b4f5e36188f24ad24f20ce867b6bb02c93424..a368f4f5b611cd48ab4389b31e0332854d0f63bc 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,6 +21,8 @@ module Tags
       else
         error('Failed to remove tag')
       end
+    rescue GitHooksService::PreReceiveError => ex
+      error(ex.message)
     end
 
     def error(message, return_code = 400)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index ad86b4f9f4250967ebb7ad8b24a1e3e15001bd41..bf7e76ec59e42f9e46dcee0163084ff519d5450d 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -103,7 +103,7 @@ class TodoService
   #
   def merge_request_build_failed(merge_request)
     create_build_failed_todo(merge_request, merge_request.author)
-    create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+    create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
   end
 
   # When a new commit is pushed to a merge request we should:
@@ -121,7 +121,7 @@ class TodoService
   #
   def merge_request_build_retried(merge_request)
     mark_pending_todos_as_done(merge_request, merge_request.author)
-    mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+    mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
   end
   
   # When a merge request could not be automatically merged due to its unmergeable state we should:
@@ -129,7 +129,7 @@ class TodoService
   #  * create a todo for a merge_user
   #
   def merge_request_became_unmergeable(merge_request)
-    create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+    create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
   end
   
   # When create a note we should:
@@ -201,10 +201,12 @@ class TodoService
   def update_todos_state_by_ids(ids, current_user, state)
     todos = current_user.todos.where(id: ids)
 
-    # Only return those that are not really on that state
-    marked_todos = todos.where.not(state: state).update_all(state: state)
+    # Only update those that are not really on that state
+    todos = todos.where.not(state: state)
+    todos_ids = todos.pluck(:id)
+    todos.update_all(state: state)
     current_user.update_todos_count_cache
-    marked_todos
+    todos_ids
   end
 
   def create_todos(users, attributes)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index bc0653cb634ec765391c5a496f7207d94219b4e2..833da5bc5d1e6a368b6b4716252f7e8a6e5f1aab 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -7,7 +7,7 @@ module Users
     end
 
     def execute(user, options = {})
-      unless current_user.admin? || current_user == user
+      unless Ability.allowed?(current_user, :destroy_user, user)
         raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
       end
 
@@ -26,6 +26,8 @@ module Users
         ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
       end
 
+      move_issues_to_ghost_user(user)
+
       # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
       namespace = user.namespace
       user_data = user.destroy
@@ -33,5 +35,22 @@ module Users
 
       user_data
     end
+
+    private
+
+    def move_issues_to_ghost_user(user)
+      # Block the user before moving issues to prevent a data race.
+      # If the user creates an issue after `move_issues_to_ghost_user`
+      # runs and before the user is destroyed, the destroy will fail with
+      # an exception. We block the user so that issues can't be created
+      # after `move_issues_to_ghost_user` runs and before the destroy happens.
+      user.block
+
+      ghost_user = User.ghost
+
+      user.issues.update_all(author_id: ghost_user.id)
+
+      user.reload
+    end
   end
 end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index fad741531eab983e2cebac8915c5a40ccb725708..8f6f5b937c4b42d1f3521fe93ce5314365d8b5f7 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -93,9 +93,7 @@ module Users
     end
 
     def current_authorizations_per_project
-      current_authorizations.each_with_object({}) do |row, hash|
-        hash[row.project_id] = row
-      end
+      current_authorizations.index_by(&:project_id)
     end
 
     def current_authorizations
@@ -115,11 +113,23 @@ module Users
     # Returns a union query of projects that the user is authorized to access
     def project_authorizations_union
       relations = [
+        # Personal projects
         user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
-        user.groups_projects.select_for_project_authorization,
+
+        # Projects the user is a member of
         user.projects.select_for_project_authorization,
+
+        # Projects of groups the user is a member of
+        user.groups_projects.select_for_project_authorization,
+
+        # Projects of subgroups of groups the user is a member of
+        user.nested_groups_projects.select_for_project_authorization,
+
+        # Projects shared with groups the user is a member of
         user.groups.joins(:shared_projects).select_for_project_authorization,
-        user.nested_projects.select_for_project_authorization
+
+        # Projects shared with subgroups of groups the user is a member of
+        user.nested_groups.joins(:shared_projects).select_for_project_authorization
       ]
 
       Gitlab::SQL::Union.new(relations)
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 86f317dcd18f51d691c78e01e48e0614234c91fe..e84944ed4116716568b112fbae92d21642edc3a6 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader
     File.join(self.class.artifacts_cache_path, @build.artifacts_path)
   end
 
-  def file_storage?
-    self.class.storage == CarrierWave::Storage::File
-  end
-
   def filename
     file.try(:filename)
   end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index cfcb877cc3e5b83df013184465d8e3959f0ca5f3..109eb2fea0b5750a2079ed7998a049c090ecbc8b 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,9 +1,10 @@
 class AttachmentUploader < GitlabUploader
+  include RecordsUploads
   include UploaderHelper
 
   storage :file
 
   def store_dir
-    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+    "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
   end
 end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 265cea2d2c62892e69c6d37c0fe4a12b9ba360aa..66d3bcb998aa0ff7602bf83009a31986287838f7 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,10 +1,11 @@
 class AvatarUploader < GitlabUploader
+  include RecordsUploads
   include UploaderHelper
 
   storage :file
 
   def store_dir
-    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+    "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
   end
 
   def exists?
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 23b7318827c5d586de4f7c4043b408ff80141609..d6ccf0dc92c13df48001c569942750c71b5ab5c6 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,30 +1,53 @@
 class FileUploader < GitlabUploader
+  include RecordsUploads
   include UploaderHelper
+
   MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
 
   storage :file
 
-  attr_accessor :project, :secret
+  def self.absolute_path(upload_record)
+    File.join(
+      self.dynamic_path_segment(upload_record.model),
+      upload_record.path
+    )
+  end
 
-  def initialize(project, secret = nil)
-    @project = project
-    @secret = secret || self.class.generate_secret
+  # Returns the part of `store_dir` that can change based on the model's current
+  # path
+  #
+  # This is used to build Upload paths dynamically based on the model's current
+  # namespace and path, allowing us to ignore renames or transfers.
+  #
+  # model - Object that responds to `path_with_namespace`
+  #
+  # Returns a String without a trailing slash
+  def self.dynamic_path_segment(model)
+    File.join(CarrierWave.root, base_dir, model.path_with_namespace)
   end
 
-  def base_dir
-    "uploads"
+  attr_accessor :project
+  attr_reader :secret
+
+  def initialize(project, secret = nil)
+    @project = project
+    @secret = secret || generate_secret
   end
 
   def store_dir
-    File.join(base_dir, @project.path_with_namespace, @secret)
+    File.join(dynamic_path_segment, @secret)
   end
 
   def cache_dir
     File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
   end
 
-  def secure_url
-    File.join("/uploads", @secret, file.filename)
+  def model
+    project
+  end
+
+  def relative_path
+    self.file.path.sub("#{dynamic_path_segment}/", '')
   end
 
   def to_markdown
@@ -35,17 +58,27 @@ class FileUploader < GitlabUploader
     filename = image_or_video? ? self.file.basename : self.file.filename
     escaped_filename = filename.gsub("]", "\\]")
 
-    markdown = "[#{escaped_filename}](#{self.secure_url})"
+    markdown = "[#{escaped_filename}](#{secure_url})"
     markdown.prepend("!") if image_or_video? || dangerous?
 
     {
       alt:      filename,
-      url:      self.secure_url,
+      url:      secure_url,
       markdown: markdown
     }
   end
 
-  def self.generate_secret
+  private
+
+  def dynamic_path_segment
+    self.class.dynamic_path_segment(model)
+  end
+
+  def generate_secret
     SecureRandom.hex
   end
+
+  def secure_url
+    File.join('/uploads', @secret, file.filename)
+  end
 end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 02d7c601d6c08667b58d453738d477c194bf8a9f..d662ba6820cecd585b3afbb0212c4d0cc2780103 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,4 +1,18 @@
 class GitlabUploader < CarrierWave::Uploader::Base
+  def self.absolute_path(upload_record)
+    File.join(CarrierWave.root, upload_record.path)
+  end
+
+  def self.base_dir
+    'uploads'
+  end
+
+  delegate :base_dir, to: :class
+
+  def file_storage?
+    self.class.storage == CarrierWave::Storage::File
+  end
+
   # Reduce disk IO
   def move_to_cache
     true
@@ -8,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base
   def move_to_store
     true
   end
+
+  # Designed to be overridden by child uploaders that have a dynamic path
+  # segment -- that is, a path that changes based on mutable attributes of its
+  # associated model
+  #
+  # For example, `FileUploader` builds the storage path based on the associated
+  # project model's `path_with_namespace` value, which can change when the
+  # project or its containing namespace is moved or renamed.
+  def relative_path
+    self.file.path.sub("#{root}/", '')
+  end
 end
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4c127f29250053fc82c5b665df1eb3f014820eae
--- /dev/null
+++ b/app/uploaders/records_uploads.rb
@@ -0,0 +1,34 @@
+module RecordsUploads
+  extend ActiveSupport::Concern
+
+  included do
+    after :store,   :record_upload
+    before :remove, :destroy_upload
+  end
+
+  private
+
+  # After storing an attachment, create a corresponding Upload record
+  #
+  # NOTE: We're ignoring the argument passed to this callback because we want
+  # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
+  # `Tempfile` object the callback gets.
+  #
+  # Called `after :store`
+  def record_upload(_tempfile)
+    return unless file_storage?
+    return unless file.exists?
+
+    Upload.record(self)
+  end
+
+  # Before removing an attachment, destroy any Upload records at the same path
+  #
+  # Called `before :remove`
+  def destroy_upload(*args)
+    return unless file_storage?
+    return unless file
+
+    Upload.remove_path(relative_path)
+  end
+end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 35fd1ed23f87f8327a774baec75febed58125006..7635c20ab3a545559c0db1d9b8984c00b1b67d63 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,15 +1,15 @@
 # Extra methods for uploader
 module UploaderHelper
-  IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+  IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
   # We recommend using the .mp4 format over .mov. Videos in .mov format can
   # still be used but you really need to make sure they are served with the
   # proper MIME type video/mp4 and not video/quicktime or your videos won't play
   # on IE >= 9.
   # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
-  VIDEO_EXT = %w[mp4 m4v mov webm ogv]
+  VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
   # These extension types can contain dangerous code and should only be embedded inline with
   # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
-  DANGEROUS_EXT = %w[svg]
+  DANGEROUS_EXT = %w[svg].freeze
 
   def image?
     extension_match?(IMAGE_EXT)
@@ -27,6 +27,8 @@ module UploaderHelper
     extension_match?(DANGEROUS_EXT)
   end
 
+  private
+
   def extension_match?(extensions)
     return false unless file
 
@@ -40,8 +42,4 @@ module UploaderHelper
 
     extensions.include?(extension.downcase)
   end
-
-  def file_storage?
-    self.class.storage == CarrierWave::Storage::File
-  end
 end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 09bfa613cbe6d6b71d899cf90f11db28c585aa3f..94542125d43166177d69e91154352cf024f94ea5 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -18,7 +18,7 @@
 #   end
 #
 class AddressableUrlValidator < ActiveModel::EachValidator
-  DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }
+  DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze
 
   def validate_each(record, attribute, value)
     unless valid_url?(value)
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10ff44031c6616101a1e0ae7ad85e368ef606360
--- /dev/null
+++ b/app/validators/duration_validator.rb
@@ -0,0 +1,17 @@
+# DurationValidator
+#
+# Validate the format conforms with ChronicDuration
+#
+# Example:
+#
+#   class ApplicationSetting < ActiveRecord::Base
+#     validates :default_artifacts_expire_in, presence: true, duration: true
+#   end
+#
+class DurationValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    ChronicDuration.parse(value)
+  rescue ChronicDuration::DurationParseError
+    record.errors.add(attribute, "is not a correct duration")
+  end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37a314adee6c4207ebec153816683c06fe31715b
--- /dev/null
+++ b/app/validators/importable_url_validator.rb
@@ -0,0 +1,11 @@
+# ImportableUrlValidator
+#
+# This validator blocks projects from using dangerous import_urls to help
+# protect against Server-side Request Forgery (SSRF).
+class ImportableUrlValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if Gitlab::UrlBlocker.blocked_url?(value)
+      record.errors.add(attribute, "imports are not allowed from that URL")
+    end
+  end
+end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index eb3ed31b65bbd089735f231ca376ae4ae32f3d51..77ca033e97f70aed54321fe4f081aeea365d6c76 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -35,12 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
     users
   ].freeze
 
+  WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
+                       preview blob blame raw files create_dir find_file
+                       artifacts graphs refs badges].freeze
+
+  STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
+
   def self.valid?(value)
     !reserved?(value) && follow_format?(value)
   end
 
-  def self.reserved?(value)
-    RESERVED.include?(value)
+  def self.reserved?(value, strict: false)
+    if strict
+      STRICT_RESERVED.include?(value)
+    else
+      RESERVED.include?(value)
+    end
   end
 
   def self.follow_format?(value)
@@ -54,7 +64,9 @@ class NamespaceValidator < ActiveModel::EachValidator
       record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
     end
 
-    if reserved?(value)
+    strict = record.is_a?(Group) && record.parent_id
+
+    if reserved?(value, strict: strict)
       record.errors.add(attribute, "#{value} is a reserved name")
     end
   end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 36279daa743224b1a75e80abdabb6126b3efbf28..ee2ae65be7bc6f1b4872409158c0c96d346d94df 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
   #  without tree as reserved name routing can match 'group/project' as group name,
   #  'tree' as project name and 'deploy_keys' as route.
   #
-  RESERVED = (NamespaceValidator::RESERVED -
-              %w[dashboard help ci admin search notes services assets profile public] +
-              %w[tree commits wikis new edit create update logs_tree
-                 preview blob blame raw files create_dir find_file]).freeze
+  RESERVED = (NamespaceValidator::STRICT_RESERVED -
+              %w[dashboard help ci admin search notes services assets profile public]).freeze
 
   def self.valid?(value)
     !reserved?(value)
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 6c48328da4f2d9a3838ad51d7099de265467c4fb..6a5e170ddd84da28fe49ac91e40b6c7337468fff 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -16,4 +16,4 @@
   - else
     .empty-state
       .text-center
-        %h4 There are no abuse reports! #{emoji_icon 'tada'}
+        %h4 There are no abuse reports! #{emoji_icon('tada')}
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 9175b3d3f964dd93d0cb8491ff02372c4b239709..e403a9da616d69d0cefc6fad9b3df3303de52db4 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -48,7 +48,7 @@
   .form-actions
     = f.submit 'Save', class: 'btn btn-save append-right-10'
     - if @appearance.persisted?
-      = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+      = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
 
     - if @appearance.updated_at
       %span.pull-right
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 749c74b811022abb714a9234120f5ec1acccbfc4..3eab065bb9f9aa9ab282971e5388ba5cbccaac05 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -212,8 +212,16 @@
       .col-sm-10
         = f.number_field :max_artifacts_size, class: 'form-control'
         .help-block
-          Set the maximum file size each jobs's artifacts can have
-          = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
+          Set the maximum file size for each job's artifacts
+          = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+    .form-group
+      = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :default_artifacts_expire_in, class: 'form-control'
+        .help-block
+          Set the default expiration time for each job's artifacts.
+          0 for unlimited.
+          = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
 
   - if Gitlab.config.registry.enabled
     %fieldset
@@ -352,6 +360,29 @@
           Generate API key at
           %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
 
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :unique_ips_limit_enabled do
+            = f.check_box :unique_ips_limit_enabled
+            Limit sign in from multiple ips
+          %span.help-block#unique_ip_help_block
+            Helps prevent malicious users hide their activity
+
+    .form-group
+      = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+        .help-block
+          Maximum number of unique IPs per user
+
+    .form-group
+      = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+        .help-block
+          How many seconds an IP will be counted towards the limit
+
   %fieldset
     %legend Abuse reports
     .form-group
@@ -373,7 +404,7 @@
             Enable Sentry
           .help-block
             Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
-            %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+            %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
 
     .form-group
       = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index c689b26d6e6f02409c9c2db3b78af54bd46a2b56..061f8991b11a7c21aefe6795341aaa05183da95b 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -26,4 +26,4 @@
 
   .form-actions
     = f.submit 'Submit', class: "btn btn-save wide"
-    = link_to "Cancel", admin_applications_path, class: "btn btn-default"
+    = link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..1378dde52ab356091c8109c0c7ece1be219e58ad
--- /dev/null
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -0,0 +1,8 @@
+- page_title "Impersonation Tokens", @user.name, "Users"
+= render 'admin/users/head'
+
+.row.prepend-top-default
+  .col-lg-12
+    = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
+
+    = render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c1a9f8d6ddd49d8c19d5d745a972f9f9a934263b
--- /dev/null
+++ b/app/views/admin/projects/_projects.html.haml
@@ -0,0 +1,32 @@
+.js-projects-list-holder
+  - if @projects.any?
+    %ul.projects-list.content-list
+      - @projects.each_with_index do |project|
+        %li.project-row
+          .controls
+            - if project.archived
+              %span.label.label-warning archived
+            %span.badge
+              = storage_counter(project.statistics.storage_size)
+            = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
+            = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
+          .title
+            = link_to [:admin, project.namespace.becomes(Namespace), project] do
+              .dash-project-avatar
+                .avatar-container.s40
+                  = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+              %span.project-full-name
+                %span.namespace-name
+                  - if project.namespace
+                    = project.namespace.human_name
+                    \/
+                %span.project-name.filter-title
+                  = project.name
+
+          - if project.description.present?
+            .description
+              = markdown_field(project, :description)
+
+    = paginate @projects, theme: 'gitlab'
+  - else
+    .nothing-here-block No projects found
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index c35945c5a357634ecd8d17099cc83d4ca1201967..3301f55b8a8b89891dc55ba3ba84c0d2089eb55e 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,40 +7,24 @@
 %div{ class: container_class }
   .top-area
     .prepend-top-default
-      = 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'
-
-            - if params[:visibility_level].present?
-              = hidden_field_tag 'visibility_level', params[:visibility_level]
-
-            - if params[:sort].present?
-              = hidden_field_tag 'sort', params[:sort]
-
-            - if params[:personal].present?
-              = hidden_field_tag 'visibility_level', 'true'
-
-            - if params[:archived].present?
-              = hidden_field_tag 'archived', 'true'
-
-            = icon("search", class: "search-icon")
-
-          .dropdown
-            - toggle_text = 'Namespace'
-            - if params[:namespace_id].present?
-              - namespace = Namespace.find(params[:namespace_id])
-              - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
-            = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
-            .dropdown-menu.dropdown-select.dropdown-menu-align-right
-              = dropdown_title('Namespaces')
-              = dropdown_filter("Search for Namespace")
-              = dropdown_content
-              = dropdown_loading
-          = render 'shared/projects/dropdown'
-          = link_to new_project_path, class: 'btn btn-new' do
-            New Project
-          = button_tag "Search", class: "btn btn-primary btn-search hide"
+      .search-holder
+        = render 'shared/projects/search_form', autofocus: true, icon: true
+        .dropdown
+          - toggle_text = 'Namespace'
+          - if params[:namespace_id].present?
+            = hidden_field_tag :namespace_id, params[:namespace_id]
+            - namespace = Namespace.find(params[:namespace_id])
+            - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
+          = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+          .dropdown-menu.dropdown-select.dropdown-menu-align-right
+            = dropdown_title('Namespaces')
+            = dropdown_filter("Search for Namespace")
+            = dropdown_content
+            = dropdown_loading
+        = render 'shared/projects/dropdown'
+        = link_to new_project_path, class: 'btn btn-new' do
+          New Project
+        = button_tag "Search", class: "btn btn-primary btn-search hide"
 
     %ul.nav-links
       - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
@@ -58,35 +42,4 @@
         = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
           Public
 
-  .projects-list-holder
-    - if @projects.any?
-      %ul.projects-list.content-list
-        - @projects.each_with_index do |project|
-          %li.project-row
-            .controls
-              - if project.archived
-                %span.label.label-warning archived
-              %span.badge
-                = storage_counter(project.statistics.storage_size)
-              = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
-              = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
-            .title
-              = link_to [:admin, project.namespace.becomes(Namespace), project] do
-                .dash-project-avatar
-                  .avatar-container.s40
-                    = project_icon(project, alt: '', class: 'avatar project-avatar s40')
-                %span.project-full-name
-                  %span.namespace-name
-                    - if project.namespace
-                      = project.namespace.human_name
-                      \/
-                  %span.project-name.filter-title
-                    = project.name
-
-            - if project.description.present?
-              .description
-                = markdown_field(project, :description)
-
-      = paginate @projects, theme: 'gitlab'
-    - else
-      .nothing-here-block No projects found
+  = render 'projects'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index deb62845e1ce0de190e1603f9a271210ee575a1f..d4d166ab7b63c72230bb96ac488181c124f32ab1 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -14,6 +14,8 @@
       = runner.short_sha
   %td
     = runner.description
+  %td
+    = runner.version
   %td
     - if runner.shared?
       n/a
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index d725e477044555edbe65943bee1f1d46a3c41655..7d26864d0f353c3f8f1e290ef39a863d7da53023 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -67,6 +67,7 @@
           %th Type
           %th Runner token
           %th Description
+          %th Version
           %th Projects
           %th Jobs
           %th Tags
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9984e733956afeaf1a0a62983db62be2fd22422f..be41c33b853a63364c3632de9f4067a0bbc54681 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -2,11 +2,13 @@
   = @user.name
   - if @user.blocked?
     %span.cred (Blocked)
+  - if @user.internal?
+    %span.cred (Internal)
   - if @user.admin
     %span.cred (Admin)
 
   .pull-right
-    - unless @user == current_user || @user.blocked?
+    - if @user != current_user && @user.can?(:log_in)
       = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
     = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
       %i.fa.fa-pencil-square-o
@@ -21,4 +23,6 @@
     = link_to "SSH keys", keys_admin_user_path(@user)
   = nav_link(controller: :identities) do
     = link_to "Identities", admin_user_identities_path(@user)
+  = nav_link(controller: :impersonation_tokens) do
+    = link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user)
 .append-bottom-default
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 3b5c713ac2d275c21aad0f5e1c6999820631a80a..a756cb7243af7a28d4bcde0ea77f440987d70433 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -34,7 +34,7 @@
             - if user.access_locked?
               %li
                 = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
-          - if user.can_be_removed?
+          - if user.can_be_removed? && can?(current_user, :destroy_user, @user)
             %li.divider
             %li
               = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 76b1291fe10b1856b6dedd6087397f0239c359bd..840d843f069d9c2e8dcdc5b3a3fa87c57cef13cd 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -173,7 +173,7 @@
         .panel-heading
           Remove user
         .panel-body
-          - if @user.can_be_removed?
+          - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
             %p Deleting a user has the following effects:
             %ul
               %li All user content like authored issues, snippets, comments will be removed
@@ -189,3 +189,6 @@
                 %strong= @user.solo_owned_groups.map(&:name).join(', ')
               %p
                 You must transfer ownership or delete these groups before you can delete this user.
+            - else
+              %p
+                You don't have access to delete this user.
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index e3305e21e96c8c6aba6d3d5a0a35aad8522a46fd..a1ef34dc58836c4f53fb6b6a2a3064585891e5cb 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -4,7 +4,7 @@
     %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
       class: (award_state_class(awards, current_user)),
       data: { placement: "bottom", title: award_user_list(awards, current_user) } }
-      = emoji_icon(emoji, sprite: false)
+      = emoji_icon(emoji)
       %span.award-control-text.js-counter
         = awards.count
 
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index dc76599b7767fe4ded0b42c8c80e2292de72b7b1..89d991abe548065c9bc3f9c3d535a65a6a1c8634 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -2,10 +2,9 @@
   = render "events/event_last_push", event: @last_push
 
 .nav-block
-  - if current_user
-    .controls
-      = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
-        %i.fa.fa-rss
+  .controls
+    = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
+      %i.fa.fa-rss
   = render 'shared/event_filter'
 
 .content_list
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 23c145ebbb42b6e706dab45e851d7e1e2ac5e49f..13eaba41f4cefb014548e446c4330b94c421e7c3 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -6,7 +6,9 @@
     = nav_link(page: explore_groups_path) do
       = link_to explore_groups_path, title: 'Explore groups' do
         Explore Groups
-  - if current_user.can_create_group?
-    .nav-controls
+  .nav-controls
+    = render 'shared/groups/search_form'
+    = render 'shared/groups/dropdown'
+    - if current_user.can_create_group?
       = link_to new_group_path, class: "btn btn-new" do
         New Group
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 48b0fd504f4f8c5645787d7d4b6f9cc45e1ee850..600ee63a5c09f31624cc68c7ee889350e7791cb0 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -13,8 +13,7 @@
         Explore projects
 
   .nav-controls
-    = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
-      = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
+    = render 'shared/projects/search_form'
     = render 'shared/projects/dropdown'
     - if current_user.can_create_project?
       = link_to new_project_path, class: 'btn btn-new' do
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index aa57df14c239fc621fb027b763058a26f4e31650..190ad4b40a5c698d329b2a357995ca6694894e51 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,6 +1,5 @@
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+  = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
 
 - page_title    "Activity"
 - header_title  "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6c3bf1a2b3bca94ed614017152db484decb093dd
--- /dev/null
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -0,0 +1,6 @@
+.js-groups-list-holder
+  %ul.content-list
+    - @group_members.each do |group_member|
+      = render 'shared/groups/group', group: group_member.group, group_member: group_member
+
+  = paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 1a679c51774552f23e852311c8ca8ab6057cf14c..73ab2c95ff98b0f545ef202dac420f6b23c92fb1 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -5,9 +5,4 @@
 - if @group_members.empty?
   = render 'empty_state'
 - else
-  %ul.content-list
-    - @group_members.each do |group_member|
-      - group = group_member.group
-      = render 'shared/groups/group', group: group, group_member: group_member
-
-  = paginate @group_members, theme: 'gitlab'
+  = render 'groups'
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index bdea1064096b32f874a9ddcd660d6a6bf2af063d..06fb531b546c211c5ae19d2b299bf0e14b7aa4de 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
   xml.link    href: url_for(params), rel: "self", type: "application/atom+xml"
   xml.link    href: issues_dashboard_url, rel: "alternate", type: "text/html"
   xml.id      issues_dashboard_url
-  xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+  xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
 
   xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
 end
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 653052f7c546924d1b2a67b1639ec0296ce6a4a2..10867140d4fee3896d72762b593f4db1d1af8f1c 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,17 +1,13 @@
 - page_title "Issues"
 - header_title  "Issues", issues_dashboard_path(assignee_id: current_user.id)
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues")
+  = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
 
 .top-area
   = render 'shared/issuable/nav', type: :issues
   .nav-controls
-    - if current_user
-      = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
-        = icon('rss')
-        %span.icon-label
-          Subscribe
+    = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
+      = icon('rss')
     = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
 
 = render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 917bfbd47e92d9094814695535319d5075933687..505b475f55bfa5bfc31393d28790a3630deba85d 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,11 +1,11 @@
-- page_title "Milestones"
-- header_title "Milestones", dashboard_milestones_path
+- page_title 'Milestones'
+- header_title 'Milestones', dashboard_milestones_path
 
 .top-area
-  = render 'shared/milestones_filter'
+  = render 'shared/milestones_filter', counts: @milestone_states
 
   .nav-controls
-    = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
+    = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true
 
 .milestones
   %ul.content-list
@@ -15,4 +15,4 @@
     - else
       - @milestones.each do |milestone|
         = render 'milestone', milestone: milestone
-  = paginate @milestones, theme: "gitlab"
+  = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index fb5be63b47236f2171f8915d8c22bd1de6ecf1c9..13f7a8ddcec67d6177ba719329ffb07dece9b881 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -1,7 +1,7 @@
 xml.instruct!
 xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
   xml.title   "Activity"
-  xml.link    href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  xml.link    href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml"
   xml.link    href: dashboard_projects_url, rel: "alternate", type: "text/html"
   xml.id      dashboard_projects_url
   xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 4f36a4a1c739f94629b0dd004249f390f1ce71e0..eef794dbd51540cc144f1582ea85faa97926748f 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,17 +1,17 @@
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+  = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
 
 - page_title    "Projects"
 - header_title  "Projects", dashboard_projects_path
 
-- if @projects.any? || params[:filter_projects]
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+- if @projects.any? || params[:name]
   = render 'dashboard/projects_head'
 
 - if @last_push
   = render "events/event_last_push", event: @last_push
 
-- if @projects.any? || params[:filter_projects]
+- if @projects.any? || params[:name]
   = render 'projects'
 - else
   = render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 70705923d42b9cf64d3536abce06cff1c7372a31..162ae153b1c53b5856e911e0f979038129bf33f7 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -6,7 +6,7 @@
 - if @last_push
   = render "events/event_last_push", event: @last_push
 
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
   = render 'projects'
 - else
   %h3 You don't have starred projects yet
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index a3993d5ef16709162ffb39e2b40f48bf821c1f30..d0c12aa57aefbe593e332aaa639bf8b28b721309 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -36,9 +36,14 @@
 
   - if todo.pending?
     .todo-actions
-      = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
+      = link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do
         Done
         = icon('spinner spin')
-      = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden'  do
+      = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
         Undo
         = icon('spinner spin')
+  - else
+    .todo-actions
+      = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
+        Add todo
+        = icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 16a5713948a9a901e1bad81d8fe35de9d66444ae..d31ced004a0badc69afc72de3c66199c7a182da1 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -19,9 +19,12 @@
 
     .nav-controls
       - if @todos.any?(&:pending?)
-        = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+        = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
           Mark all as done
           = icon('spinner spin')
+        = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
+          Undo mark all as done
+          = icon('spinner spin')
 
   .todos-filters
     .row-content-block.second-block
@@ -46,19 +49,19 @@
             = 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, default_label: 'Action' } })
-        .pull-right
-          .dropdown.inline.prepend-left-10
-            %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+        .filter-item.sort-filter
+          .dropdown
+            %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' }
               %span.light
               - if @sort.present?
                 = sort_options_hash[@sort]
               - else
                 = sort_title_recently_created
               = icon('chevron-down')
-            %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+            %ul.dropdown-menu.dropdown-menu-sort
               %li
-                = link_to todos_filter_path(sort: sort_value_priority) do
-                  = sort_title_priority
+                = link_to todos_filter_path(sort: sort_value_label_priority) do
+                  = sort_title_label_priority
                 = link_to todos_filter_path(sort: sort_value_recently_created) do
                   = sort_title_recently_created
                 = link_to todos_filter_path(sort: sort_value_oldest_created) do
@@ -67,12 +70,16 @@
 
 .js-todos-all
   - if @todos.any?
-    .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
-      .panel.panel-default.panel-small.panel-without-border
-        %ul.content-list.todos-list
-          = render @todos
-    = paginate @todos, theme: "gitlab"
-
+    .js-todos-list-container
+      .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
+        .panel.panel-default.panel-small.panel-without-border
+          %ul.content-list.todos-list
+            = render @todos
+      = paginate @todos, theme: "gitlab"
+    .js-nothing-here-container.todos-all-done.hidden
+      = render "shared/empty_states/icons/todos_all_done.svg"
+      %h4.text-center
+        You're all done!
   - elsif current_user.todos.any?
     .todos-all-done
       = render "shared/empty_states/icons/todos_all_done.svg"
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 951f03083bfc3abe0930dc003d8cee9cc0c9c83d..a039756c7e2c2091e018bf328bcc407c7d357085 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,6 +1,6 @@
 - if inject_u2f_api?
   - content_for :page_specific_javascripts do
-    = page_specific_javascript_tag('u2f.js')
+    = page_specific_javascript_bundle_tag('u2f')
 
 %div
   = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 5a44ec45b7b70adccec65e58d19ca34618dea265..a2f6a7ab1cb5dad46f97dbb46590c681f9c2daed 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -4,11 +4,11 @@
       .devise-errors
         = devise_error_messages!
       .form-group
-        = f.label :name
+        = f.label :name, 'Full name'
         = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
       .username.form-group
         = f.label :username
-        = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
+        = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
         %p.validation-error.hide Username is already taken.
         %p.validation-success.hide Username is available.
         %p.validation-pending.hide Checking username availability...
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 2deadbeeceb304b55c609c5cdcb28b7f10e0629f..ee452add3944d261bbfa76fcd63b5fea891e380e 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -2,5 +2,5 @@
 %tr.notes_holder{ class: ('hide' unless expanded) }
   %td.notes_line{ colspan: 2 }
   %td.notes_content
-    .content
+    .content{ class: ('hide' unless expanded) }
       = render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ca9e0e8728a47b3532b0cd8e4534514477c23174
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml
@@ -0,0 +1,6 @@
+- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
+  .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
+    .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue",
+                                                             "aria-label" => "Resolve all discussions in a new issue",
+                                                             "data-container" => "body" }
+      = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..df5546a1e32d76573e8f4e3c7c5bcfa5d382ef79
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_discussion.html.haml
@@ -0,0 +1,8 @@
+- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
+  %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
+                                 "inline-template" => true }
+    .btn-group{ role: "group", "v-if" => "showButton" }
+      .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue",
+                                                               "aria-label" => "Resolve this discussion in a new issue",
+                                                               "data-container" => "body" }
+        = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index dfdbdf1f969ce98942c210b9a5c0a8aea5665209..2789391819cf0a506da2f9a72b345bd0e7b397b4 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -11,6 +11,8 @@
           = link_to_reply_discussion(discussion, line_type)
         = render "discussions/resolve_all", discussion: discussion
         - if discussion.for_merge_request?
-          = render "discussions/jump_to_next", discussion: discussion
+          .btn-group.discussion-actions
+            = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
+            = render "discussions/jump_to_next", discussion: discussion
     - else
       = link_to_reply_discussion(discussion)
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index a196561f381a778b3f6baeed765cfd072cd5807a..82aa51f9778bf900f8f079ee8e64068cbc5271c5 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -27,6 +27,7 @@
       = hidden_field_tag :state, @pre_auth.state
       = hidden_field_tag :response_type, @pre_auth.response_type
       = hidden_field_tag :scope, @pre_auth.scope
+      = hidden_field_tag :nonce, @pre_auth.nonce
       = submit_tag "Authorize", class: "btn btn-success wide pull-left"
     = form_tag oauth_authorization_path, method: :delete do
       = hidden_field_tag :client_id, @pre_auth.client.uid
@@ -34,4 +35,5 @@
       = hidden_field_tag :state, @pre_auth.state
       = hidden_field_tag :response_type, @pre_auth.response_type
       = hidden_field_tag :scope, @pre_auth.scope
+      = hidden_field_tag :nonce, @pre_auth.nonce
       = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
deleted file mode 100644
index 49bd9acd2db156f207e4d239a47fdc5b53521f1f..0000000000000000000000000000000000000000
--- a/app/views/emojis/index.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-.emoji-menu
-  = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji"
-  .emoji-menu-content
-    - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
-      %h5.emoji-menu-title
-        = Gitlab::AwardEmoji::CATEGORIES[category]
-      %ul.clearfix.emoji-menu-list
-        - emojis.each do |emoji|
-          %li.pull-left.text-center.emoji-menu-list-item
-            %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
-              = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 7890e717aa7c60e74b3a4bc40fb4c95e73427f3d..158061579f6e2df64a67897c32d98a52ae141b58 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -4,12 +4,12 @@ xml.entry do
   xml.id      "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
   xml.link    href: event_feed_url(event)
   xml.title   truncate(event_feed_title(event), length: 80)
-  xml.updated event.created_at.xmlschema
+  xml.updated event.updated_at.xmlschema
   xml.media   :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
 
   xml.author do
     xml.name event.author_name
-    xml.email event.author_email
+    xml.email event.author_public_email
   end
 
   xml.summary(type: "xhtml") do |summary|
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index f08c96df309c9c68fa33091ea7578e34f38547da..64b5a733b77987b54c1dd0e78626f0727a584aa9 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -15,6 +15,6 @@
         = link_to note.attachment.url, target: '_blank' do
           = image_tag note.attachment.url, class: 'note-image-attach'
       - else
-        = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
+        = link_to note.attachment.url, target: '_blank',  class: 'note-file-attach' do
           %i.fa.fa-paperclip
           = note.attachment_identifier
diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..794c6d1d17069b9463b3c04a7535f1153aef4524
--- /dev/null
+++ b/app/views/explore/groups/_groups.html.haml
@@ -0,0 +1,6 @@
+.js-groups-list-holder
+  %ul.content-list
+    - @groups.each do |group|
+      = render 'shared/groups/group', group: group
+
+  = paginate @groups, theme: 'gitlab'
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c8d95b52156333b47c90bc5c268bd2c9ed27ec9c
--- /dev/null
+++ b/app/views/explore/groups/_nav.html.haml
@@ -0,0 +1,8 @@
+.top-area
+  %ul.nav-links
+    = nav_link(page: explore_groups_path) do
+      = link_to explore_groups_path do
+        Explore Groups
+  .nav-controls
+    = render 'shared/groups/search_form'
+    = render 'shared/groups/dropdown'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 73cf6e87eb4388e0c9d143a87170c8a69111d8c2..bb2cd0d44c836679f67350db67fc40cf2ef9f0c3 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -5,41 +5,9 @@
   = render 'dashboard/groups_head'
 - else
   = render 'explore/head'
+  = render 'nav'
 
-.row-content-block.clearfix
-  .pull-left
-    = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f|
-      = hidden_field_tag :sort, @sort
-      .form-group
-        = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false
-      .form-group
-        = button_tag 'Search', class: "btn btn-default"
-
-  .pull-right
-    .dropdown.inline
-      %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
-        %span.light
-        - if @sort.present?
-          = sort_options_hash[@sort]
-        - else
-          = sort_title_recently_created
-        = icon('chevron-down')
-      %ul.dropdown-menu.dropdown-menu-align-right
-        %li
-          = link_to explore_groups_path(sort: sort_value_recently_created) do
-            = sort_title_recently_created
-          = link_to explore_groups_path(sort: sort_value_oldest_created) do
-            = sort_title_oldest_created
-          = link_to explore_groups_path(sort: sort_value_recently_updated) do
-            = sort_title_recently_updated
-          = link_to explore_groups_path(sort: sort_value_oldest_updated) do
-            = sort_title_oldest_updated
-
-%ul.content-list
-  - @groups.each do |group|
-    = render 'shared/groups/group', group: group
-  - unless @groups.present?
-    .nothing-here-block No public groups
-
-
-= paginate @groups, theme: "gitlab"
+- if @groups.present?
+  = render 'groups'
+- else
+  .nothing-here-block No public groups
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 614b5431779351042cf94fbe407de1c689206aef..e0a2a1e9c96daba3af2fbce1ede9a9585bb8d7b5 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,10 +1,17 @@
-%ul.nav-links
-  = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
-    = link_to trending_explore_projects_path do
-      Trending
-  = nav_link(page: starred_explore_projects_path) do
-    = link_to starred_explore_projects_path do
-      Most stars
-  = nav_link(page: explore_projects_path) do
-    = link_to explore_projects_path do
-      All
+.top-area
+  %ul.nav-links
+    = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
+      = link_to trending_explore_projects_path do
+        Trending
+    = nav_link(page: starred_explore_projects_path) do
+      = link_to starred_explore_projects_path do
+        Most stars
+    = nav_link(page: explore_projects_path) do
+      = link_to explore_projects_path do
+        All
+
+  .nav-controls
+    - unless current_user
+      = render 'shared/projects/search_form'
+      = render 'shared/projects/dropdown'
+    = render 'filter'
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 42b50481b9d3d59f26680024e332d7866f5706ec..ec4617551030505ad0bce37237e5d9b70d4102f2 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -6,10 +6,5 @@
 - else
   = render 'explore/head'
 
-.top-area
-  = render 'explore/projects/nav'
-
-  .nav-controls
-    = render 'filter'
-
+= render 'explore/projects/nav'
 = render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 71cc4d87b1f3ee1559307283d88b13250a9c92e7..d7851c7999033fb263b524879c427743ed7b39df 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -2,10 +2,9 @@
   = render "events/event_last_push", event: @last_push
 
 .nav-block
-  - if current_user
-    .controls
-      = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do
-        %i.fa.fa-rss
+  .controls
+    = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
+      %i.fa.fa-rss
   = render 'shared/event_filter'
 
 .content_list
diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..20de1b4c973cf2ff973f1aba2369c9632c7ae771
--- /dev/null
+++ b/app/views/groups/_create_chat_team.html.haml
@@ -0,0 +1,16 @@
+.form-group
+  = f.label :create_chat_team, class: 'control-label' do
+    %span.mattermost-icon
+      = custom_icon('icon_mattermost')
+    Mattermost
+  .col-sm-10
+    .checkbox.js-toggle-container
+      = f.label :create_chat_team do
+        .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false)
+        Create a Mattermost team for this group
+      %br
+      %small.light.js-toggle-content
+        Mattermost URL:
+        = Settings.mattermost.host
+        %span> /
+        %span{ "data-bind-out" => "create_chat_team" }
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..873504099d4c0d5b51c23160d0464351d3dcb5c7
--- /dev/null
+++ b/app/views/groups/_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: container_class }
+        = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
+          = link_to group_path(@group), title: 'Group Home' do
+            %span
+              Home
+
+        = nav_link(path: 'groups#activity') do
+          = link_to activity_group_path(@group), title: 'Activity' do
+            %span
+              Activity
diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d554bc23743136b03a62ae862d3906e403821435
--- /dev/null
+++ b/app/views/groups/_head_issues.html.haml
@@ -0,0 +1,19 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: container_class }
+        = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
+          = link_to issues_group_path(@group), title: 'List' do
+            %span
+              List
+
+        = nav_link(path: 'labels#index') do
+          = link_to group_labels_path(@group), title: 'Labels' do
+            %span
+              Labels
+
+        = nav_link(path: 'milestones#index') do
+          = link_to group_milestones_path(@group), title: 'Milestones' do
+            %span
+              Milestones
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2454e7355a71d7218827703b3ab3c5803658f3f3
--- /dev/null
+++ b/app/views/groups/_settings_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: container_class }
+        = nav_link(path: 'groups#edit') do
+          = link_to edit_group_path(@group), title: 'General' do
+            %span
+              General
+
+        = nav_link(path: 'groups#projects') do
+          = link_to projects_group_path(@group), title: 'Projects' do
+            %span
+              Projects
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
index aaad265b3ee316c1a2dcb0b946a1331434e60db8..3969e56f937b18d4ab08af47860754f2b25a299d 100644
--- a/app/views/groups/activity.html.haml
+++ b/app/views/groups/activity.html.haml
@@ -1,8 +1,8 @@
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+  = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
 
-- page_title    "Activity"
+- page_title "Activity"
+= render 'groups/head'
 
 %section.activities
   = render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2706e8692d16793c3a707f6d0abc712c5c7f056c..80a77dab97f7b5d2d687e789cf10902cca6d1d6d 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,4 @@
+= render "groups/settings_head"
 .panel.panel-default.prepend-top-default
   .panel-heading
     Group settings
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 0cc6466d34eadaaa16348e7e9b7c9fb60ac00597..469768d83f28acc55c108988a0c6ccddcb444384 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
   xml.link    href: url_for(params), rel: "self", type: "application/atom+xml"
   xml.link    href: issues_group_url, rel: "alternate", type: "text/html"
   xml.id      issues_group_url
-  xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+  xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
 
   xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
 end
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 83edb71969288a715ab8a99e992f0653eee1b762..f4c17dc2d167074fa6e8fe0fcd0f66d38f893016 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,18 +1,17 @@
 - page_title "Issues"
+= render "head_issues"
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
+  = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
 
 - if group_issues(@group).exists?
   .top-area
     = render 'shared/issuable/nav', type: :issues
-    - if current_user
-      .nav-controls
-        = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
-          = icon('rss')
-          %span.icon-label
-            Subscribe
-        = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+    .nav-controls
+      = link_to params.merge(rss_url_options), class: 'btn' do
+        = icon('rss')
+        %span.icon-label
+          Subscribe
+      = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
 
   = render 'shared/issuable/filter', type: :issues
 
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 45325d6bc4b95f39be28c7ef1098c269cd5a64b4..2bc00fb16c866941f9d2193556849eb265836763 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,4 +1,5 @@
 - page_title 'Labels'
+= render "groups/head_issues"
 
 .top-area.adjust
   .nav-text
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index cd5388fe40269abac2b9bb2466537a8cbc0fb499..6893168f03979a1e31982f2fdfb05e6be399beb4 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,7 +1,8 @@
 - page_title "Milestones"
+= render "groups/head_issues"
 
 .top-area
-  = render 'shared/milestones_filter'
+  = render 'shared/milestones_filter', counts: @milestone_states
 
   .nav-controls
     - if can?(current_user, :admin_milestones, @group)
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 38d63fd9acc64be3e3e7d7bb607cfdff2136c54b..000c7af2326858748848505d922624ecf6ba9468 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -16,6 +16,8 @@
 
   = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
 
+  = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+
   .form-group
     .col-sm-offset-2.col-sm-10
       = render 'shared/group_tips'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 2e7e5e5c30950386fb05194bafa2ba0320991dbd..83bdd654f27de249d3180b23182057cff64b1c2b 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,4 +1,4 @@
-- page_title "Projects"
+= render "groups/settings_head"
 
 .panel.panel-default.prepend-top-default
   .panel-heading
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index b68bf444d27f5e0385dac03ba607d15ff1cd60e2..914091dfd157f0ff57614732fa1b829ed84914ad 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -1,7 +1,7 @@
 xml.instruct!
 xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
   xml.title   "#{@group.name} activity"
-  xml.link    href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  xml.link    href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml"
   xml.link    href: group_url(@group), rel: "alternate", type: "text/html"
   xml.id      group_url(@group)
   xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b040f404ac42f781ab05f7ed54870bc54406ecb3..18997baa998908e5cf484b66e978706102f16d1c 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,9 +1,9 @@
 - @no_container = true
 
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+  = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
 
+= render 'groups/head'
 = render 'groups/home_panel'
 
 
@@ -11,8 +11,7 @@
   .top-area
     = render 'groups/show_nav'
     .nav-controls
-      = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
-        = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+      = render 'shared/projects/search_form'
       = render 'shared/projects/dropdown'
       - if can? current_user, :create_projects, @group
         = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index 8610ae7e0ef710136039ff24fcba062743e6ea41..be80908313919fd5a5546eb3d38e4e24ad433031 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -1,5 +1,6 @@
 - @no_container = true
 
+= render 'head'
 = render 'groups/home_panel'
 
 .groups-header{ class: container_class }
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 705e20112fa356b1b66c9127e578ec0f02c33dac..2684f16c3735bf2579df5b9465623b66bcdf65d9 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -163,7 +163,7 @@
                     .key g
                     .key g
                   %td
-                    Go to graphs
+                    Go to repository charts
                 %tr
                   %td.shortcut
                     .key g
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 31631887317849fec46dd73c641251388508dd68..f93b6b63426eca6a2335e03f9c870e162fc257b9 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -17,7 +17,7 @@
     %br
     Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
     %br
-    Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+    Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
     - if current_application_settings.help_page_text.present?
       %hr
       = markdown_field(current_application_settings, :help_page_text)
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 87f9b5039892db7d676076f14aaf339b64664df8..1fb2c6271add60d1065a224401e2f8e2784c9781 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -410,7 +410,7 @@
     :javascript
       $('#js-project-dropdown').glDropdown({
         data: function (term, callback) {
-          Api.projects(term, "last_activity_at", function (data) {
+          Api.projects(term, { order_by: 'last_activity_at' }, function (data) {
             callback(data);
           });
         },
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index e18bd47798b379355b707b2c553223dd53a7a942..e6058617ac99c0772b058d2f1e2694060180731e 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -33,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', rel: 'noopener noreferrer'
           %td
             = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
           %td.job-status
@@ -50,7 +50,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
           %td
-            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
           %td.import-target
             %fieldset.row
             .input-group
@@ -70,7 +70,7 @@
       - @incompatible_repos.each do |repo|
         %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
           %td
-            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
           %td.import-target
           %td.import-actions-job-status
             = label_tag 'Incompatible Project', nil, class: 'label label-danger'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d5b88709a349c163a616bf77a343f1f688a7491b..7456799ca0e62b1d41eba94ac89463ae737731b0 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -43,7 +43,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo["id"]}" }
           %td
-            = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+            = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
             = import_project_target(repo['namespace']['path'], repo['name'])
           %td.import-actions.job-status
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 336becd229e8d3d6a5efcd0ea7840f6cd7d53c4c..c5800a1cca0db475ae0010a732ad3879389536c7 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -13,7 +13,7 @@
     %li
       %p
         Go to
-        #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+        #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}.
     %li
       %p
         Make sure you're logged into the account that owns the projects you'd like to import.
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5e01af008bec56fc06a60036fd9ee4de2f66a4fe..60de6bfe8163beb5cf71b11a2117038597b6481b 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -36,7 +36,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://code.google.com/p/#{project.import_source}", target: "_blank"
+            = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
           %td
             = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
           %td.job-status
@@ -53,7 +53,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo.id}" }
           %td
-            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
             #{current_user.username}/#{repo.name}
           %td.import-actions.job-status
@@ -63,7 +63,7 @@
       - @incompatible_repos.each do |repo|
         %tr{ id: "repo_#{repo.id}" }
           %td
-            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
           %td.import-actions-job-status
             = label_tag "Incompatible Project", nil, class: "label label-danger"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 968318741447739462ddfd5834e35e6582c6c5ef..23a884480552ace1c011d29fedf795d3eeb7f985 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -2,12 +2,12 @@ xml.entry do
   xml.id      namespace_project_issue_url(issue.project.namespace, issue.project, issue)
   xml.link    href: namespace_project_issue_url(issue.project.namespace, issue.project, issue)
   xml.title   truncate(issue.title, length: 80)
-  xml.updated issue.created_at.xmlschema
+  xml.updated issue.updated_at.xmlschema
   xml.media   :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
 
   xml.author do
     xml.name issue.author_name
-    xml.email issue.author_email
+    xml.email issue.author_public_email
   end
 
   xml.summary issue.title
@@ -26,7 +26,7 @@ xml.entry do
   if issue.assignee
     xml.assignee do
       xml.name issue.assignee.name
-      xml.email issue.assignee.email
+      xml.email issue.assignee_public_email
     end
   end
 end
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 65887aacbafb8ee20c1c6dcc3c60c993a895e623..04e2d4b63e6b2c7fb429b1f3ea3b9cafc611b3ac 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -2,5 +2,5 @@
   %p
     = icon('circle', class: 'cgreen')
     Integration is active for
-    = link_to koding_project_url, target: '_blank' do
+    = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
       #{current_application_settings.koding_url}
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 302c1794628851efc90990b7cfdee39023975946..f6d8bb08a646a4c24bc9b8d2aa0261d4b7245fb0 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,7 +28,9 @@
   = stylesheet_link_tag "application", media: "all"
   = stylesheet_link_tag "print",       media: "print"
 
-  = javascript_include_tag(*webpack_asset_paths("application"))
+  = javascript_include_tag(*webpack_asset_paths("runtime"))
+  = javascript_include_tag(*webpack_asset_paths("common"))
+  = javascript_include_tag(*webpack_asset_paths("main"))
 
   - if content_for?(:page_specific_javascripts)
     = yield :page_specific_javascripts
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 3daa1e90a8c335fd41db5b9042af4a14d6399446..769f6fb01512a41616ba71a51301d10fae1c24a2 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -4,7 +4,6 @@
 - if project
   :javascript
     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)}",
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1717ed6b365ec7be851895f34e631a78748fe18b..a35a918d5011f599fe12dd227f7702c033f11913 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,7 +1,7 @@
 .page-with-sidebar{ class: page_gutter_class }
   - if defined?(nav) && nav
     .layout-nav
-      %div{ class: container_class }
+      .container-fluid
         = render "layouts/nav/#{nav}"
   .content-wrapper{ class: "#{layout_nav_class}" }
     = yield :sub_nav
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 19bd9b6d5c99ef7ba47aa938ff83396362be1040..36543edc040f91e562494ecea3a5a441a7279b2d 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
 !!! 5
 %html{ lang: "en", class: "#{page_class}" }
   = render "layouts/head"
-  %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
+  %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
     = Gon::Base.render_data
 
     = render "layouts/header/default", title: header_title
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index f8986893776eb573596132fb02023b337a86a5fe..5fde5c2613e3c5da3b6e1acba6391c830d12671d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -19,7 +19,7 @@
         %ul.nav.navbar-nav
           %li.hidden-sm.hidden-xs
             = render 'layouts/search' unless current_controller?(:search)
-          %li.visible-sm.visible-xs
+          %li.visible-sm-inline-block.visible-xs-inline-block
             = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
               = icon('search')
           - if current_user
@@ -36,6 +36,10 @@
                 = icon('bell fw')
                 %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
                   = todos_count_format(todos_pending_count)
+            - if current_user.can_create_project?
+              %li
+                = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+                  = icon('plus fw')
             - if Gitlab::Sherlock.enabled?
               %li
                 = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
@@ -51,8 +55,6 @@
                     = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
                   %li
                     = link_to "Settings", profile_path, aria: { label: "Settings" }
-                  %li
-                    = link_to "Help", help_path, aria: { label: "Help" }
                   %li.divider
                   %li
                     = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
@@ -65,7 +67,7 @@
         = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
           = brand_header_logo
 
-      %h1.title= title
+      %h1.title{ class: ('initializing' if @has_group_title) }= title
 
       = yield :header_content
 
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..53268cc22f81faa8b67a840ce684b11c490967c3
--- /dev/null
+++ b/app/views/layouts/mailer.html.haml
@@ -0,0 +1,72 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{ lang: "en" }
+  %head
+    %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+    %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+    %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
+    %title= message.subject
+    :css
+      /* CLIENT-SPECIFIC STYLES */
+      body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+      table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+      img { -ms-interpolation-mode: bicubic; }
+
+      /* iOS BLUE LINKS */
+      a[x-apple-data-detectors] {
+          color: inherit !important;
+          text-decoration: none !important;
+          font-size: inherit !important;
+          font-family: inherit !important;
+          font-weight: inherit !important;
+          line-height: inherit !important;
+      }
+
+      /* ANDROID MARGIN HACK */
+      body { margin:0 !important; }
+      div[style*="margin: 16px 0"] { margin:0 !important; }
+
+      @media only screen and (max-width: 639px) {
+          body, #body {
+              min-width: 320px !important;
+          }
+          table.wrapper {
+              width: 100% !important;
+              min-width: 320px !important;
+          }
+          table.wrapper > tbody > tr > td {
+              border-left: 0 !important;
+              border-right: 0 !important;
+              border-radius: 0 !important;
+              padding-left: 10px !important;
+              padding-right: 10px !important;
+          }
+      }
+  %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+    %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+      %tbody
+        %tr.line
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
+        %tr.header
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+            = header_logo
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+            %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+              %tbody
+                %tr
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+                    %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
+                      %tbody
+                        = yield
+
+        %tr.footer
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+            %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
+            %div
+              %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
+              &middot;
+              %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
+            %div
+              You're receiving this email because of your account on
+              = succeed "." do
+                %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6a9c6ced9cc43f8bde433f7878b6f6bca50077d0
--- /dev/null
+++ b/app/views/layouts/mailer.text.haml
@@ -0,0 +1,5 @@
+= yield
+
+You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
+Manage all notifications: #{profile_notifications_url}
+Help: #{help_url}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 19a947af4ca2686703cee91dbdf94ea70c443ec0..d068c895fa39059d53380dab110f3594b644b8b1 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -33,7 +33,7 @@
           Abuse Reports
           %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
 
-    - if askimet_enabled?
+    - if akismet_enabled?
       = nav_link(controller: :spam_logs) do
         = link_to admin_spam_logs_path, title: "Spam Logs" do
           %span
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5d4178f03d71310cc3045e17b1c2df057866cdfe..15285ee32a3fc7056ecc571301ed958feaaf9d65 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -24,16 +24,16 @@
     = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
       %span
         Issues
-        (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))})
+      .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
   = nav_link(path: 'dashboard#merge_requests') do
     = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
       %span
         Merge Requests
-        (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))})
+      .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
   = nav_link(controller: 'dashboard/snippets') do
     = link_to dashboard_snippets_path, title: 'Snippets' do
       %span
         Snippets
   %li.divider
   %li
-    = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab'
+    = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f3539fd372df2b5ce5d151301b8a3ae4c751a262..8605380848d64fc2c43738a6416353ef70220130 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,27 +1,14 @@
-= render 'layouts/nav/group_settings'
 .scrolling-tabs-container{ class: nav_control_class }
   .fade-left
     = icon('angle-left')
   .fade-right
     = icon('angle-right')
   %ul.nav-links.scrolling-tabs
-    = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
+    = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
       = link_to group_path(@group), title: 'Home' do
         %span
           Group
-    = nav_link(path: 'groups#activity') do
-      = link_to activity_group_path(@group), title: 'Activity' do
-        %span
-          Activity
-    = nav_link(controller: [:group, :labels]) do
-      = link_to group_labels_path(@group), title: 'Labels' do
-        %span
-          Labels
-    = nav_link(controller: [:group, :milestones]) do
-      = link_to group_milestones_path(@group), title: 'Milestones' do
-        %span
-          Milestones
-    = nav_link(path: 'groups#issues') do
+    = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
       = link_to issues_group_path(@group), title: 'Issues' do
         %span
           Issues
@@ -33,7 +20,12 @@
           Merge Requests
           - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
           %span.badge.count= number_with_delimiter(merge_requests.count)
-    = nav_link(controller: [:group_members]) do
+    = nav_link(path: 'group_members#index') do
       = link_to group_group_members_path(@group), title: 'Members' do
         %span
           Members
+    - if current_user && can?(current_user, :admin_group, @group)
+      = nav_link(path: %w[groups#projects groups#edit]) do
+        = link_to edit_group_path(@group), title: 'Settings' do
+          %span
+            Settings
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
deleted file mode 100644
index 30feb6813b4a0d998d62ab2136a87fdbb1133f81..0000000000000000000000000000000000000000
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- if current_user
-  - can_admin_group = can?(current_user, :admin_group, @group)
-  - can_edit = can?(current_user, :admin_group, @group)
-
-  - if can_admin_group || can_edit
-    .controls
-      .dropdown.group-settings-dropdown
-        %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
-          = icon('cog')
-          = icon('caret-down')
-        %ul.dropdown-menu.dropdown-menu-align-right
-          - if can_admin_group
-            = nav_link(path: 'groups#projects') do
-              = link_to 'Projects', projects_group_path(@group), title: 'Projects'
-          - if can_edit && can_admin_group
-            %li.divider
-            %li
-              = link_to 'Edit Group', edit_group_path(@group)
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 7883823b21ea065f768cec678f0d1ff38a62c5ad..299dace34069072aded8fecbf98ab09d21f8aeee 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,60 +1,27 @@
-- if current_user
-  .controls
-    .dropdown.project-settings-dropdown
-      %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
-        = icon('cog')
-        = icon('caret-down')
-      %ul.dropdown-menu.dropdown-menu-align-right
-        - can_edit = can?(current_user, :admin_project, @project)
-
-        = render 'layouts/nav/project_settings', can_edit: can_edit
-
-        - if can_edit
-          %li.divider
-          %li
-            = link_to edit_project_path(@project) do
-              Edit Project
-
+- can_edit = can?(current_user, :admin_project, @project)
 .scrolling-tabs-container{ class: nav_control_class }
   .fade-left
     = icon('angle-left')
   .fade-right
     = icon('angle-right')
   %ul.nav-links.scrolling-tabs
-    = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
+    = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
       = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
         %span
           Project
 
-    = nav_link(path: 'projects#activity') do
-      = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
-        %span
-          Activity
-
     - if project_nav_tab? :files
-      = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
+      = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do
         = link_to project_files_path(@project), title: 'Repository',  class: 'shortcuts-tree' do
           %span
             Repository
 
-    - if project_nav_tab? :pipelines
-      = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
-        = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
-          %span
-            Pipelines
-
     - if project_nav_tab? :container_registry
       = nav_link(controller: %w(container_registry)) do
         = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
           %span
             Registry
 
-    - if project_nav_tab? :graphs
-      = nav_link(controller: %w(graphs)) do
-        = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs',  class: 'shortcuts-graphs' do
-          %span
-            Graphs
-
     - if project_nav_tab? :issues
       = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
         = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
@@ -70,6 +37,12 @@
             Merge Requests
             %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
 
+    - if project_nav_tab? :pipelines
+      = nav_link(controller: [:pipelines, :builds, :environments]) do
+        = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+          %span
+            Pipelines
+
     - if project_nav_tab? :wiki
       = nav_link(controller: :wikis) do
         = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
@@ -82,18 +55,41 @@
           %span
             Snippets
 
-    -# Global shortcut to network page for compatibility
+    - if project_nav_tab? :settings
+      = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
+        = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
+          %span
+            Settings
+    - else
+      = nav_link(path: %w[members#show]) do
+        = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do
+          %span
+            Settings
+
+    -# Shortcut to Project > Activity
+    %li.hidden
+      = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
+        %span
+          Activity
+
+    -# Shortcut to Repository > Graph (formerly, Network)
     - if project_nav_tab? :network
       %li.hidden
         = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
-          Network
+          Graph
+
+    -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
+    - unless @project.empty_repo?
+      %li.hidden
+        = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
+          Charts
 
-    -# Shortcut to create a new issue
+    -# Shortcut to Issues > New Issue
     %li.hidden
       = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do
         Create a new issue
 
-    -# Shortcut to builds page
+    -# Shortcut to Pipelines > Jobs
     - if project_nav_tab? :builds
       %li.hidden
         = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
deleted file mode 100644
index 665725f6862cdf24131e8df20edfa066fdc42179..0000000000000000000000000000000000000000
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- if project_nav_tab? :team
-  = nav_link(controller: [:members, :teams]) do
-    = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
-      %span
-        Members
-- if can_edit
-  = nav_link(controller: :deploy_keys) do
-    = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
-      %span
-        Deploy Keys
-  = nav_link(controller: :integrations) do
-    = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
-      %span
-        Integrations
-  = nav_link(controller: :protected_branches) do
-    = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
-      %span
-        Protected Branches
-
-  - if @project.feature_available?(:builds, current_user)
-    = nav_link(controller: :ci_cd) do
-      = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
-        %span
-          CI/CD Pipelines
-  = nav_link(controller: :pages) do
-    = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
-      %span
-        Pages
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc698aad7db2714254bf75a65bf61a906..0000000000000000000000000000000000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
-  %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
-    GitLab (job failed)
-
-%h3
-  Project:
-  = link_to namespace_project_url(@project.namespace, @project) do
-    = @project.name
-
-%p
-  Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
-  Author: #{@build.pipeline.git_author_name}
-%p
-  Branch: #{@build.ref}
-%p
-  Stage: #{@build.stage}
-%p
-  Job: #{@build.name}
-%p
-  Message: #{@build.pipeline.git_commit_message}
-
-%p
-  Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b060e4714f890e33f1df91beda250ba..0000000000000000000000000000000000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status:   <%= @build.status %>
-Commit:   <%= @build.pipeline.short_sha %>
-Author:   <%= @build.pipeline.git_author_name %>
-Branch:   <%= @build.ref %>
-Stage:    <%= @build.stage %>
-Job:      <%= @build.name %>
-Message:  <%= @build.pipeline.git_commit_message %>
-
-Url:      <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9db3166a915ab257adae6665b252371..0000000000000000000000000000000000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
-  %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
-    GitLab (job successful)
-
-%h3
-  Project:
-  = link_to namespace_project_url(@project.namespace, @project) do
-    = @project.name
-
-%p
-  Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
-  Author: #{@build.pipeline.git_author_name}
-%p
-  Branch: #{@build.ref}
-%p
-  Stage: #{@build.stage}
-%p
-  Job: #{@build.name}
-%p
-  Message: #{@build.pipeline.git_commit_message}
-
-%p
-  Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64fe66daa390891a1f0839b7920f5113..0000000000000000000000000000000000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status:   <%= @build.status %>
-Commit:   <%= @build.pipeline.short_sha %>
-Author:   <%= @build.pipeline.git_author_name %>
-Branch:   <%= @build.ref %>
-Stage:    <%= @build.stage %>
-Job:      <%= @build.name %>
-Message:  <%= @build.pipeline.git_commit_message %>
-
-Url:      <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index d9ebbaa27046af868a60c49a9f1d2b17afbfd081..85a1aea3a61c50c803b6ea64f22f34b985c2b47f 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -1,179 +1,109 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{ lang: "en" }
-  %head
-    %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
-    %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
-    %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
-    %title= message.subject
-    :css
-      /* CLIENT-SPECIFIC STYLES */
-      body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
-      table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
-      img { -ms-interpolation-mode: bicubic; }
-
-      /* iOS BLUE LINKS */
-      a[x-apple-data-detectors] {
-          color: inherit !important;
-          text-decoration: none !important;
-          font-size: inherit !important;
-          font-family: inherit !important;
-          font-weight: inherit !important;
-          line-height: inherit !important;
-      }
-
-      /* ANDROID MARGIN HACK */
-      body { margin:0 !important; }
-      div[style*="margin: 16px 0"] { margin:0 !important; }
-
-      @media only screen and (max-width: 639px) {
-          body, #body {
-              min-width: 320px !important;
-          }
-          table.wrapper {
-              width: 100% !important;
-              min-width: 320px !important;
-          }
-          table.wrapper > tbody > tr > td {
-              border-left: 0 !important;
-              border-right: 0 !important;
-              border-radius: 0 !important;
-              padding-left: 10px !important;
-              padding-right: 10px !important;
-          }
-      }
-  %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
-    %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+%tr.alert
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
+    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
       %tbody
-        %tr.line
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
-        %tr.header
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
-            %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
         %tr
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
-            %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+            %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+            Your pipeline has failed.
+%tr.spacer
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+    &nbsp;
+%tr.section
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+    %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+      %tbody
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+            - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+            - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+            %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+              = namespace_name
+            \/
+            %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+              = @project.name
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+              %tbody
+                %tr
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
+                      = @pipeline.ref
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
               %tbody
                 %tr
-                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
-                    %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
-                      %tbody
-                        %tr.alert
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
-                            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
-                              %tbody
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
-                                    %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
-                                    Your pipeline has failed.
-                        %tr.spacer
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
-                            &nbsp;
-                        %tr.section
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
-                            %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
-                              %tbody
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
-                                    - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
-                                    - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
-                                    %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
-                                      = namespace_name
-                                    \/
-                                    %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
-                                      = @project.name
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
-                                              = @pipeline.ref
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
-                                              = @pipeline.short_sha
-                                            - if @merge_request
-                                              in
-                                              %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
-                                                = @merge_request.to_reference
-                                    .commit{ style: "color:#5c5c5c;font-weight:300;" }
-                                      = @pipeline.git_commit_message.truncate(50)
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          - commit = @pipeline.commit
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            - if commit.author
-                                              %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
-                                                = commit.author.name
-                                            - else
-                                              %span
-                                                = commit.author_name
-                        %tr.spacer
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
-                            &nbsp;
-                        - failed = @pipeline.statuses.latest.failed
-                        %tr.pre-section
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
-                            Pipeline
-                            %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
-                              = "\##{@pipeline.id}"
-                            had
-                            = failed.size
-                            failed
-                            #{'build'.pluralize(failed.size)}.
-                        %tr.warning
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
-                            Logs may contain sensitive data. Please consider before forwarding this email.
-                        %tr.section
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
-                            %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
-                              %tbody
-                                - failed.each do |build|
-                                  %tr.build-state
-                                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
-                                      %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                        %tbody
-                                          %tr
-                                            %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
-                                              %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
-                                            %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
-                                              = build.stage
-                                    %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
-                                      = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
-                                  %tr.build-log
-                                    - if build.has_trace?
-                                      %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
-                                        %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
-                                          = build.trace_html(last_lines: 10).html_safe
-                                    - else
-                                      %td{ colspan: "2" }
-        %tr.footer
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
-            %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
-            %div
-              %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
-              &middot;
-              %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
-            %div
-              You're receiving this email because of your account on
-              = succeed "." do
-                %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+                      = @pipeline.short_sha
+                    - if @merge_request
+                      in
+                      %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
+                        = @merge_request.to_reference
+            .commit{ style: "color:#5c5c5c;font-weight:300;" }
+              = @pipeline.git_commit_message.truncate(50)
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+              %tbody
+                %tr
+                  - commit = @pipeline.commit
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    - if commit.author
+                      %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
+                        = commit.author.name
+                    - else
+                      %span
+                        = commit.author_name
+%tr.spacer
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+    &nbsp;
+- failed = @pipeline.statuses.latest.failed
+%tr.pre-section
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
+    Pipeline
+    %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+      = "\##{@pipeline.id}"
+    had
+    = failed.size
+    failed
+    #{'build'.pluralize(failed.size)}.
+%tr.warning
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
+    Logs may contain sensitive data. Please consider before forwarding this email.
+%tr.section
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
+    %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
+      %tbody
+        - failed.each do |build|
+          %tr.build-state
+            %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+              %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+                %tbody
+                  %tr
+                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
+                      %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
+                    %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
+                      = build.stage
+            %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+              = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
+          %tr.build-log
+            - if build.has_trace?
+              %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
+                %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
+                  = build.trace_html(last_lines: 10).html_safe
+            - else
+              %td{ colspan: "2" }
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index ab91c7ef35056461893d70877d24ffc51b99b900..520a2fc7d68ea5ca1c3c0198f75a6291916f9d63 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
 <% end -%>
 
 <% end -%>
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 8add2e1820622ef2af69236b3dac6bbc22ace908..19d4add06f598d3a3bad5d1e9b8fa89e67d01de2 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,154 +1,84 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{ lang: "en" }
-  %head
-    %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
-    %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
-    %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
-    %title= message.subject
-    :css
-      /* CLIENT-SPECIFIC STYLES */
-      body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
-      table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
-      img { -ms-interpolation-mode: bicubic; }
-
-      /* iOS BLUE LINKS */
-      a[x-apple-data-detectors] {
-          color: inherit !important;
-          text-decoration: none !important;
-          font-size: inherit !important;
-          font-family: inherit !important;
-          font-weight: inherit !important;
-          line-height: inherit !important;
-      }
-
-      /* ANDROID MARGIN HACK */
-      body { margin:0 !important; }
-      div[style*="margin: 16px 0"] { margin:0 !important; }
-
-      @media only screen and (max-width: 639px) {
-          body, #body {
-              min-width: 320px !important;
-          }
-          table.wrapper {
-              width: 100% !important;
-              min-width: 320px !important;
-          }
-          table.wrapper > tbody > tr > td {
-              border-left: 0 !important;
-              border-right: 0 !important;
-              border-radius: 0 !important;
-              padding-left: 10px !important;
-              padding-right: 10px !important;
-          }
-      }
-  %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
-    %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+%tr.success
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
       %tbody
-        %tr.line
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
-        %tr.header
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
-            %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
         %tr
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
-            %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+            %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+            Your pipeline has passed.
+%tr.spacer
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+    &nbsp;
+%tr.section
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+    %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+      %tbody
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+            - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+            - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+            %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+              = namespace_name
+            \/
+            %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+              = @project.name
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+              %tbody
+                %tr
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
+                      = @pipeline.ref
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+              %tbody
+                %tr
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+                      = @pipeline.short_sha
+                    - if @merge_request
+                      in
+                      %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
+                        = @merge_request.to_reference
+            .commit{ style: "color:#5c5c5c;font-weight:300;" }
+              = @pipeline.git_commit_message.truncate(50)
+        %tr
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
               %tbody
                 %tr
-                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
-                    %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
-                      %tbody
-                        %tr.success
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
-                            %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
-                              %tbody
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
-                                    %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
-                                    Your pipeline has passed.
-                        %tr.spacer
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
-                            &nbsp;
-                        %tr.section
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
-                            %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
-                              %tbody
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
-                                    - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
-                                    - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
-                                    %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
-                                      = namespace_name
-                                    \/
-                                    %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
-                                      = @project.name
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
-                                              = @pipeline.ref
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
-                                              = @pipeline.short_sha
-                                            - if @merge_request
-                                              in
-                                              %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
-                                                = @merge_request.to_reference
-                                    .commit{ style: "color:#5c5c5c;font-weight:300;" }
-                                      = @pipeline.git_commit_message.truncate(50)
-                                %tr
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
-                                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
-                                    %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
-                                      %tbody
-                                        %tr
-                                          - commit = @pipeline.commit
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
-                                            %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
-                                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
-                                            - if commit.author
-                                              %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
-                                                = commit.author.name
-                                            - else
-                                              %span
-                                                = commit.author_name
-                        %tr.spacer
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
-                            &nbsp;
-                        %tr.success-message
-                          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
-                            - build_count = @pipeline.statuses.latest.size
-                            - stage_count = @pipeline.stages_count
-                            Pipeline
-                            %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
-                              = "\##{@pipeline.id}"
-                            successfully completed
-                            #{build_count} #{'build'.pluralize(build_count)}
-                            in
-                            #{stage_count} #{'stage'.pluralize(stage_count)}.
-        %tr.footer
-          %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
-            %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
-            %div
-              %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
-              &middot;
-              %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
-            %div
-              You're receiving this email because of your account on
-              = succeed "." do
-                %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
+                  - commit = @pipeline.commit
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+                    %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+                  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+                    - if commit.author
+                      %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
+                        = commit.author.name
+                    - else
+                      %span
+                        = commit.author_name
+%tr.spacer
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+    &nbsp;
+%tr.success-message
+  %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
+    - build_count = @pipeline.statuses.latest.size
+    - stage_count = @pipeline.stages_count
+    Pipeline
+    %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+      = "\##{@pipeline.id}"
+    successfully completed
+    #{build_count} #{'build'.pluralize(build_count)}
+    in
+    #{stage_count} #{'stage'.pluralize(stage_count)}.
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 40e5e306426338f2df5718f7b0971340c32b3fe5..0970a3a4e0923c7130f55f0757b96ce6c24925f9 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %>
 <% build_count = @pipeline.statuses.latest.size -%>
 <% stage_count = @pipeline.stages_count -%>
 Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
index 1df04ea614eec440c1b9afcf77258422988dcdce..83ae9129807162e0a7b35e152694e232a2fc38a8 100644
--- a/app/views/profiles/_head.html.haml
+++ b/app/views/profiles/_head.html.haml
@@ -1,3 +1,2 @@
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_tag('lib/cropper.js')
   = page_specific_javascript_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index a4f4079d5563f0e84a1487eb99fe2acccb5a38b7..8a994f6d600fe220e1aff1bc771e8fa65592f727 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -93,7 +93,7 @@
       %p
         Changing your username will change path to all personal projects!
     .col-lg-9
-      = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+      = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
         .form-group
           = f.label :username, "Path", class: "label-light"
           .input-group
@@ -115,7 +115,7 @@
       %h4.prepend-top-0.danger-title
         Remove account
     .col-lg-9
-      - if @user.can_be_removed?
+      - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
         %p
           Deleting an account has the following effects:
         %ul
@@ -131,4 +131,7 @@
             %strong= @user.solo_owned_groups.map(&:name).join(', ')
           %p
             You must transfer ownership or delete these groups before you can delete your account.
+        - else
+          %p
+            You don't have access to delete this user.
 .append-bottom-default
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 51c4e8e5a73c5a252ae1a4aca6859e2fa195069e..5c5e59403658c039de2aa881d5be7e13f9cbefc3 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,11 +34,6 @@
 
       .clearfix
 
-      = form_for @user, url: profile_notifications_path, method: :put do |f|
-        %label{ for: 'user_notified_of_own_activity' }
-          = f.check_box :notified_of_own_activity
-          %span Receive notifications about your own activity
-
       %hr
       %h5
         Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
deleted file mode 100644
index 3f6efa339537f74bb466e1c343b721aa8c86a345..0000000000000000000000000000000000000000
--- a/app/views/profiles/personal_access_tokens/_form.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- 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 b10f5fc08e2bdda5cc0e5c0631a310815e78b62e..0645ecad4961d166aebb55377ed374e81025e9b1 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -24,82 +24,11 @@
 
       %hr
 
-    %h5.prepend-top-0
-      Add a Personal Access Token
-    %p.profile-settings-content
-      Pick a name for the application, and we'll give you a unique token.
-
-    = render "form", personal_access_token: @personal_access_token, scopes: @scopes
-
-    %hr
-
-    %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
-
-    - if @active_personal_access_tokens.present?
-      .table-responsive
-        %table.table.active-personal-access-tokens
-          %thead
-            %tr
-              %th Name
-              %th Created
-              %th Expires
-              %th Scopes
-              %th
-          %tbody
-            - @active_personal_access_tokens.each do |token|
-              %tr
-                %td= token.name
-                %td= token.created_at.to_date.to_s(:medium)
-                %td
-                  - if token.expires_at.present?
-                    = 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
-      .settings-message.text-center
-        You don't have any active tokens yet.
-
-    %hr
-
-    %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
-
-    - if @inactive_personal_access_tokens.present?
-      .table-responsive
-        %table.table.inactive-personal-access-tokens
-          %thead
-            %tr
-              %th Name
-              %th Created
-          %tbody
-            - @inactive_personal_access_tokens.each do |token|
-              %tr
-                %td= token.name
-                %td= token.created_at.to_date.to_s(:medium)
-
-    - else
-      .settings-message.text-center
-        There are no inactive tokens.
+    = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
 
+    = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
 
 :javascript
-  var $dateField = $('#personal_access_token_expires_at');
-  var date = $dateField.val();
-
-  new Pikaday({
-    field: $dateField.get(0),
-    theme: 'gitlab-theme',
-    format: 'yyyy-mm-dd',
-    minDate: new Date(),
-    onSelect: function(dateText) {
-      $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
-    }
-  });
-
   $("#created-personal-access-token").click(function() {
     this.select();
   });
-
-  $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d551754a2e53826e9ddcbf5e8f628afe19cb7085..c74b3249a13057c85d8977d5c64922a58493ca7f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,7 @@
             or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
     .col-lg-9
       .clearfix.avatar-image.append-bottom-default
-        = link_to avatar_icon(@user, 400), target: '_blank' do
+        = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
           = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
       %h5.prepend-top-0
         Upload new avatar
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 558a1d56151421e76806c9cc5bc866c8bcc6bfba..7ade5f00d47564e1e6e27c388fefaa31ed0b1ddd 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -4,7 +4,7 @@
 
 - if inject_u2f_api?
   - content_for :page_specific_javascripts do
-    = page_specific_javascript_tag('u2f.js')
+    = page_specific_javascript_bundle_tag('u2f')
 
 .row.prepend-top-default
   .col-lg-3
@@ -96,4 +96,3 @@
   :javascript
     var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
     $(".flash-alert").append(button);
-
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
deleted file mode 100644
index 5307e0b48cb8eb5cbbc8c558d2e13d9047688293..0000000000000000000000000000000000000000
--- a/app/views/profiles/update_username.js.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- if @user.valid?
-  :plain
-    new Flash("Username successfully changed", "notice")
-- else
-  - error = @user.errors.full_messages.first
-  :plain
-    new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 0ea733cb97852a20d107bc49c7e25a2b9fc95d25..aa0cb3e1a50947a4deffc104054b954d4c4dcb99 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -2,10 +2,9 @@
 
 %div{ class: container_class }
   .nav-block.activity-filter-block
-    - if current_user
-      .controls
-        = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
-          = icon('rss')
+    .controls
+      = link_to namespace_project_path(@project.namespace, @project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
+        = icon('rss')
 
     = render 'shared/event_filter'
 
diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..db08b77c8e0bc380a7621a2d659e155865fa3b5d
--- /dev/null
+++ b/app/views/projects/_head.html.haml
@@ -0,0 +1,20 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: container_class }
+        = nav_link(path: 'projects#show') do
+          = link_to project_path(@project), title: 'Project home', class: 'shortcuts-project' do
+            %span
+              Home
+
+        = nav_link(path: 'projects#activity') do
+          = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
+            %span
+              Activity
+
+        - if can?(current_user, :read_cycle_analytics, @project)
+          = nav_link(path: 'cycle_analytics#show') do
+            = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics', class: 'shortcuts-project-cycle-analytics' do
+              %span
+                Cycle Analytics
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 27d25a6b68204b4a27cc4b829b40b1e2c38cc5eb..61420fd0fb6cdd9ddca2dcaa47b4d1580bc4ba52 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -2,8 +2,8 @@
 
 .form-group
   .checkbox.builds-feature
-    = form.label :only_allow_merge_if_build_succeeds do
-      = form.check_box :only_allow_merge_if_build_succeeds
+    = form.label :only_allow_merge_if_pipeline_succeeds do
+      = form.check_box :only_allow_merge_if_pipeline_succeeds
       %strong Only allow merge requests to be merged if the pipeline succeeds
       %br
       %span.descr
@@ -13,3 +13,7 @@
     = form.label :only_allow_merge_if_all_discussions_are_resolved do
       = form.check_box :only_allow_merge_if_all_discussions_are_resolved
       %strong Only allow merge requests to be merged if all discussions are resolved
+  .checkbox
+    = form.label :printing_merge_request_link_enabled do
+      = form.check_box :printing_merge_request_link_enabled
+      %strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 3c0f01cbf6f683f5ce21358e80fcde39d3776d2d..27c8e3c7fca68ddf4b48ba1eb0fb743324c8e747 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,5 @@
 - page_title "Activity"
+= render "projects/head"
 
 = render 'projects/last_push'
 
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8a40281e28c8267fc6808d3904e832f0df721717..4ad77b6266dd650c64c3891e44a6feded95dc0fa 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,13 +7,8 @@
 
   #blob-content-holder.tree-holder
     .file-holder
-      .js-file-title.file-title
-        = blob_icon @blob.mode, @blob.name
-        %strong
-          = @path
-        %small= number_to_human_size @blob.size
-        .file-actions
-          = render "projects/blob/actions"
+      = render "projects/blob/header", blob: @blob
+
       .table-responsive.file-content.blame.code.js-syntax-highlight
         %table
           - current_line = 1
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
deleted file mode 100644
index 7b9cfbbd067334654313e57786ead83866ec4c81..0000000000000000000000000000000000000000
--- a/app/views/projects/blob/_actions.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-.btn-group
-  = view_on_environment_button(@commit.sha, @path, @environment) if @environment
-
-.btn-group.tree-btn-group
-  = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
-      class: 'btn btn-sm', target: '_blank'
-  -# only show normal/blame view links for text files
-  - if blob_text_viewable?(@blob)
-    - if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
-      = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
-          class: 'btn btn-sm'
-    - else
-      = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
-          class: 'btn btn-sm' unless @blob.empty?
-  = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
-      class: 'btn btn-sm'
-  = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
-      tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
-
-- if current_user
-  .btn-group{ role: "group" }
-    - if blob_text_viewable?(@blob)
-      = edit_blob_link
-    = replace_blob_link
-    = delete_blob_link
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 19fa4c78501f169c69f4a10486c2d65dd3b32eab..2b2ee6ed9871eb5255218d4d47819ce3d804380e 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,18 +18,11 @@
         - else
           = link_to title, '#'
 
-%ul.blob-commit-info.table-list.hidden-xs
+%ul.blob-commit-info.hidden-xs
   - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
   = render blob_commit, project: @project, ref: @ref
 
 #blob-content-holder.blob-content-holder
   %article.file-holder
-    .js-file-title.file-title
-      = blob_icon blob.mode, blob.name
-      %strong
-        = blob.name
-      %small
-        = number_to_human_size(blob_size(blob))
-      .file-actions.hidden-xs
-        = render "actions"
-    = render blob, blob: blob
+    = render "projects/blob/header", blob: blob
+    = render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..deeeae3d64a5ea931da5cad5e4b4ebb6019b3b7a
--- /dev/null
+++ b/app/views/projects/blob/_header.html.haml
@@ -0,0 +1,39 @@
+.js-file-title.file-title-flex-parent
+  .file-header-content
+    = blob_icon blob.mode, blob.name
+
+    %strong.file-title-name
+      = blob.name
+
+    = copy_file_path_button(blob.path)
+
+    %small
+      = number_to_human_size(blob_size(blob))
+
+  .file-actions.hidden-xs
+    .btn-group{ role: "group" }<
+      = copy_blob_content_button(blob) if blob_text_viewable?(blob)
+      = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
+      = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
+    .btn-group{ role: "group" }<
+      -# only show normal/blame view links for text files
+      - if blob_text_viewable?(blob)
+        - if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
+          = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
+              class: 'btn btn-sm'
+        - else
+          = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
+              class: 'btn btn-sm js-blob-blame-link' unless blob.empty?
+
+      = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
+          class: 'btn btn-sm'
+
+      = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
+          tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
+
+    - if current_user
+      .btn-group{ role: "group" }<
+        = edit_blob_link if blob_text_viewable?(blob)
+        = replace_blob_link
+        = delete_blob_link
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index f864702d862c30c78f1fbb3f83d2001327fa1677..ea3cecb86a94c9456a58c3b1c648be4eb1360862 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -9,7 +9,7 @@
     - else
       .nothing-here-block
         The SVG could not be displayed as it is too large, you can
-        #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+        #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
         instead.
   - else
     %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 58524418a6786d3d5619128a8375e2955b780599..7b16d266982a6d394c884f4d602d81e041c73886 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -3,17 +3,17 @@
     .nothing-here-block
       File too large, you can
       = succeed '.' do
-        = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank'
+        = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
 
 - else
   - blob.load_all_data!(@repository)
 
-  - if markup?(blob.name)
-    .file-content.wiki
-      = render_markup(blob.name, blob.data)
+  - if blob.empty?
+    .file-content.code
+      .nothing-here-block Empty file
   - else
-    - if blob.empty?
-      .file-content.code
-        .nothing-here-block Empty file
+    - if markup?(blob.name)
+      .file-content.wiki
+        = render_markup(blob.name, blob.data)
     - else
       = render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index d1f7f65bf53a3d56717a3673cf07ad8db4ebb5e0..d1d448f0d4ce5fb1c767f52714ac598472a9d1f8 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -9,20 +9,20 @@
     - line_old = line_new - @form.offset
     - line_content = capture do
       %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line}
-    %tr.line_holder{ id: line_old, class: line_class }
+    %tr.line_holder.diff-expanded{ id: line_old, class: line_class }
       - case diff_view
       - when :inline
         %td.old_line.diff-line-num{ data: { linenumber: line_old } }
-          %a{ href: "##{line_old}", data: { linenumber: line_old } }
+          %a{ href: "#", data: { linenumber: line_old }, disabled: true }
         %td.new_line.diff-line-num{ data: { linenumber: line_new } }
-          %a{ href: "##{line_new}", data: { linenumber: line_new } }
+          %a{ href: "#", data: { linenumber: line_new }, disabled: true }
         = line_content
       - when :parallel
         %td.old_line.diff-line-num{ data: { linenumber: line_old } }
-          = link_to raw(line_old), "##{line_old}"
+          %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true }
         = line_content
         %td.new_line.diff-line-num{ data: { linenumber: line_new } }
-          = link_to raw(line_new), "##{line_new}"
+          %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true }
         = line_content
 
   - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 8853801016b303b1f75f113f3aadbe96cdd118ff..3bcddcb37f1aa58e4a0d4dd4232e265d3a21422f 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
   - if @conflict
     .alert.alert-danger
       Someone edited the file the same time you did. Please check out
-      = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+      = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
       and make sure your changes will not unintentionally remove theirs.
 
   .file-editor
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index f5ca9607823c1c5462c827f99c6be36db0fa9cdc..added3f669b4570e3ab46001dfdefda881d61f22 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,16 +3,19 @@
 - page_title "Boards"
 
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
+  = page_specific_javascript_bundle_tag('filtered_search')
   = page_specific_javascript_bundle_tag('boards')
   = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
 
   %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
   %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
-  %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
+  %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
 
 = render "projects/issues/head"
 
-= render 'shared/issuable/filter', type: :boards
+.hidden-xs.hidden-sm
+  = render 'shared/issuable/search_bar', type: :boards
 
 #board-app.boards-app{ "v-cloak" => true, data: board_data }
   .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
deleted file mode 100644
index 0af40ddf8fe75461a9d906ef53e3d9c459c55d36..0000000000000000000000000000000000000000
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%board-blank-state{ "inline-template" => true,
-  "v-if" => 'list.id == "blank"' }
-  .board-blank-state
-    %p
-      Add the following default lists to your Issue Board with one click:
-    %ul.board-blank-state-list
-      %li{ "v-for" => "label in predefinedLabels" }
-        %span.label-color{ ":style" =>  "{ backgroundColor: label.color } " }
-        {{ label.title }}
-    %p
-      Starting out with the default set of lists will get you right on the way to making the most of your board.
-    %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
-      Add default lists
-    %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
-      Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 72bce4049de4dca431fcba8c65133cec7cb1df32..0bca6a786cbaa3760c940012fcdc01f56a4a15bd 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -32,4 +32,4 @@
       ":root-path" => "rootPath",
       "ref" => "board-list" }
     - if can?(current_user, :admin_list, @project)
-      = render "projects/boards/components/blank_state"
+      %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index f413a5e94c143c4e33736779656909e9943c2455..4a4dd84d5d284af18e5a8f9596c486eafd4da4f8 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -2,33 +2,13 @@
   .board-list-loading.text-center{ "v-if" => "loading" }
     = icon("spinner spin")
   - if can? current_user, :create_issue, @project
-    %board-new-issue{ "inline-template" => true,
-      ":list" => "list",
+    %board-new-issue{ ":list" => "list",
       "v-if" => 'list.type !== "done" && showIssueForm' }
-      .card.board-new-issue-form
-        %form{ "@submit" => "submit($event)" }
-          .flash-container{ "v-if" => "error" }
-            .flash-alert
-              An error occured. Please try again.
-          %label.label-light{ ":for" => 'list.id + "-title"' }
-            Title
-          %input.form-control{ type: "text",
-            "v-model" => "title",
-            "ref" => "input",
-            ":id" => 'list.id + "-title"' }
-          .clearfix.prepend-top-10
-            %button.btn.btn-success.pull-left{ type: "submit",
-              ":disabled" => 'title === ""',
-              "ref" => "submit-button" }
-              Submit issue
-            %button.btn.btn-default.pull-right{ type: "button",
-              "@click" => "cancel" }
-              Cancel
   %ul.board-list{ "ref" => "list",
     "v-show" => "!loading",
     ":data-board" => "list.id",
     ":class" => '{ "is-smaller": showIssueForm }' }
-    %board-card{ "v-for" => "(issue, index) in orderedIssues",
+    %board-card{ "v-for" => "(issue, index) in issues",
       "ref" => "issue",
       ":index" => "index",
       ":list" => "list",
@@ -37,7 +17,8 @@
       ":root-path" => "rootPath",
       ":disabled" => "disabled",
       ":key" => "issue.id" }
-    %li.board-list-count.text-center{ "v-if" => "showCount" }
+    %li.board-list-count.text-center{ "v-if" => "showCount",
+      "data-issue-id" => "-1" }
       = icon("spinner spin", "v-show" => "list.loadingMore" )
       %span{ "v-if" => "list.issues.length === list.issuesSize" }
         Showing all issues
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
deleted file mode 100644
index 891c2c4625150e5c3a9cf6a9dea70a4397b55606..0000000000000000000000000000000000000000
--- a/app/views/projects/boards/components/_card.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
-  ":index" => "index",
-  ":data-issue-id" => "issue.id",
-  "@mousedown" => "mouseDown",
-  "@mousemove" => "mouseMove",
-  "@mouseup" => "showIssue($event)" }
-  %issue-card-inner{ ":list" => "list",
-    ":issue" => "issue",
-    ":issue-link-base" => "issueLinkBase",
-    ":root-path" => "rootPath" }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19ffe73a08d37729079acd6fd53b769c5628880f..9eb610ba9c00575f45cd6241dceced3a0d1455eb 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -27,11 +27,11 @@
         = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
           Compare
 
-      = render 'projects/buttons/download', project: @project, ref: branch.name
+      = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
 
       - if can?(current_user, :push_code, @project)
         = link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
-          class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
+          class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
           method: :delete,
           data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
           remote: true,
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e63bdb38bd873998d5880390ce004a5b87f1ca3d..d3c3e40d5185a3c435f77b61adb548920402d989 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -12,12 +12,16 @@
   .form-group
     = label_tag :branch_name, nil, class: 'control-label'
     .col-sm-10
-      = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name'
+      = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
       .help-block.text-danger.js-branch-name-error
   .form-group
     = label_tag :ref, 'Create from', class: 'control-label'
     .col-sm-10
-      = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+      = hidden_field_tag :ref, params[:ref] || @project.default_branch
+      = dropdown_tag(params[:ref] || @project.default_branch,
+                     options: { toggle_class: 'js-branch-select wide',
+                                filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+                                data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
       .help-block Existing branch name, tag, or commit SHA
   .form-actions
     = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 27e81c2bec30047a199dc547700992d348580455..7eb17e887e74178257e3fa70f47c4aec17895715 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,4 +1,4 @@
-.content-block.build-header
+.content-block.build-header.top-area
   .header-content
     = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
     Job
@@ -16,7 +16,10 @@
     - if @build.user
       = render "user"
     = time_ago_with_tooltip(@build.created_at)
-  - if can?(current_user, :update_build, @build) && @build.retryable?
-    = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
+  .nav-controls
+    - if can?(current_user, :create_issue, @project) && @build.failed?
+      = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
+    - if can?(current_user, :update_build, @build) && @build.retryable?
+      = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
   %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
     = icon('angle-double-left')
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 228dad528ab6a34db64532d3875e80b18d9e385f..307010edb58c81ebab206416741fb89a1fff3afb 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,5 @@
 - @no_container = true
 - page_title "#{@build.name} (##{@build.id})", "Jobs"
-- trace_with_state = @build.trace_with_state
 = render "projects/pipelines/head", build_subnav: true
 
 %div{ class: container_class }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index b560ed21f1d3fc36c1f4e1cb66f8701ce29bd6b4..d90d4a27cd654da4ca4fff5c6472a1d556c6d5e9 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,3 +1,5 @@
+- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+
 - if !project.empty_repo? && can?(current_user, :download_code, project)
   .project-action-button.dropdown.inline>
     %button.btn{ 'data-toggle' => 'dropdown' }
@@ -24,7 +26,6 @@
           %i.fa.fa-download
           %span Download tar
 
-      - pipeline = project.pipelines.latest_successful_for(ref)
       - if pipeline
         - artifacts = pipeline.builds.latest.with_artifacts
         - if artifacts.any?
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index 5d9a776da8931dd9379b0ac136de22523e115605..a5a9e4d0621f4beb678f3b8e4794d12dec66c862 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,3 +1,3 @@
 - if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
-  = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+  = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do
     Run in IDE (Koding)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 5ea85f9fd4c9c8f2563846a6fba38c25026f7f3d..09286a1b3c68e0c203dc7ea105dd1a344ee370df 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -46,7 +46,7 @@
         %span.label.label-info triggered
       - if build.try(:allow_failure)
         %span.label.label-danger allowed to fail
-      - if build.manual?
+      - if build.action?
         %span.label.label-info manual
 
   - if pipeline_link
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
deleted file mode 100644
index 3475fa5f960908eec2c60a25cff78cdf6386f15b..0000000000000000000000000000000000000000
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ /dev/null
@@ -1,92 +0,0 @@
-- status = pipeline.status
-- show_commit = local_assigns.fetch(:show_commit, true)
-- show_branch = local_assigns.fetch(:show_branch, true)
-
-%tr.commit
-  %td.commit-link
-    = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
-
-  %td
-    = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
-      %span.pipeline-id ##{pipeline.id}
-    %span by
-    - if pipeline.user
-      = user_avatar(user: pipeline.user, size: 20)
-    - else
-      %span.api.monospace API
-    - if pipeline.latest?
-      %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
-    - if pipeline.triggered?
-      %span.label.label-primary triggered
-    - if pipeline.yaml_errors.present?
-      %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
-    - if pipeline.builds.any?(&:stuck?)
-      %span.label.label-warning stuck
-
-  %td.branch-commit
-    - if pipeline.ref && show_branch
-      .icon-container
-        = pipeline.tag? ? icon('tag') : icon('code-fork')
-      = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
-    - if show_commit
-      .icon-container.commit-icon
-        = custom_icon("icon_commit")
-      = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
-
-    %p.commit-title
-      - if commit = pipeline.commit
-        = author_avatar(commit, size: 20)
-        = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
-      - else
-        Cant find HEAD commit for this branch
-
-  %td
-    = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
-
-  %td
-    - if pipeline.duration
-      %p.duration
-        = custom_icon("icon_timer")
-        = duration_in_numbers(pipeline.duration)
-    - if pipeline.finished_at
-      %p.finished-at
-        = icon("calendar")
-        #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
-
-  %td.pipeline-actions.hidden-xs
-    .controls.pull-right
-      - artifacts = pipeline.builds.latest.with_artifacts_not_expired
-      - actions = pipeline.manual_actions
-      - if artifacts.present? || actions.any?
-        .btn-group.inline
-          - if actions.any?
-            .btn-group
-              %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
-                = custom_icon('icon_play')
-                = icon('caret-down', 'aria-hidden' => 'true')
-              %ul.dropdown-menu.dropdown-menu-align-right
-                - actions.each do |build|
-                  %li
-                    = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
-                      = custom_icon('icon_play')
-                      %span= build.name
-          - if artifacts.present?
-            .btn-group
-              %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
-                = icon("download")
-                = icon('caret-down')
-              %ul.dropdown-menu.dropdown-menu-align-right
-                - artifacts.each do |build|
-                  %li
-                    = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
-                      = icon("download")
-                      %span Download '#{build.name}' artifacts
-
-      - if can?(current_user, :update_pipeline, pipeline.project)
-        .cancel-retry-btns.inline
-          - if pipeline.retryable?
-            = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do
-              = icon("repeat")
-          - if pipeline.cancelable?
-            = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
-              = icon("remove")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 421b3db342dd7f154d43d3b8f1be5e709296119e..b5f67cae341319965980ac72e43cc8c78d3b0d75 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -1,10 +1,10 @@
 - case type.to_s
 - when 'revert'
   - label = 'Revert'
-  - target_label = 'Revert in branch'
+  - branch_label = 'Revert in branch'
 - when 'cherry-pick'
   - label = 'Cherry-pick'
-  - target_label = 'Pick into branch'
+  - branch_label = 'Pick into branch'
 
 .modal{ id: "modal-#{type}-commit" }
   .modal-dialog
@@ -15,10 +15,10 @@
       .modal-body
         = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
           .form-group.branch
-            = label_tag 'target_branch', target_label, class: 'control-label'
+            = label_tag 'start_branch', branch_label, class: 'control-label'
             .col-sm-10
-              = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
-              = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
+              = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
+              = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
 
               - if can?(current_user, :push_code, @project)
                 .js-create-merge-request-container
@@ -37,4 +37,4 @@
                 = commit_in_fork_help
 
 :javascript
-  new NewCommitForm($('.js-#{type}-form'))
+  new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 4d0b7a5ca8537f30013acb0ebddc03bfdd840d3f..a0a292d0508aea8fb05632f75ae63163d53a7fb5 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -34,8 +34,9 @@
             = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
         %li.clearfix
           = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
-        %li.clearfix
-          = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
+        - if can_collaborate_with_project?
+          %li.clearfix
+            = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
         %li.divider
         %li.dropdown-header
           Download
@@ -62,15 +63,15 @@
 
   - if @commit.status
     .well-segment.pipeline-info
-      %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
-        = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+      .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
+        = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do
           = ci_icon_for_status(@commit.status)
       Pipeline
-      = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
-      for
-      = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
-      %span.ci-status-label
-        = ci_label_for_status(@commit.status)
+      = link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace"
+      = ci_label_for_status(@commit.status)
+      - if @commit.latest_pipeline.stages.any?
+        .mr-widget-pipeline-graph
+          = render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph'
       in
       = time_interval_in_words @commit.pipelines.total_duration
 
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 33917513f37d1e2ad116cf1b24c08d10d2dbac4d..da5a676274f05a7c3fb713a1f0518dba64aee074 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -2,27 +2,7 @@
 #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
   endpoint: endpoint,
 } }
-.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"),
-  "icon_status_canceled" => custom_icon("icon_status_canceled"),
-  "icon_status_running" => custom_icon("icon_status_running"),
-  "icon_status_skipped" => custom_icon("icon_status_skipped"),
-  "icon_status_created" => custom_icon("icon_status_created"),
-  "icon_status_pending" => custom_icon("icon_status_pending"),
-  "icon_status_success" => custom_icon("icon_status_success"),
-  "icon_status_failed" => custom_icon("icon_status_failed"),
-  "icon_status_warning" => custom_icon("icon_status_warning"),
-  "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
-  "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
-  "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
-  "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
-  "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
-  "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
-  "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
-  "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
-  "icon_play" => custom_icon("icon_play"),
-  "icon_timer" => custom_icon("icon_timer"),
-  "icon_status_manual" => custom_icon("icon_status_manual"),
-} }
 
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 002e3d345dcf150857debb6030299e6b9a70ad18..6ab9a80e0837df32c3f798e9050443badbb59003 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,33 +9,34 @@
 - cache_key.push(commit.status(ref)) if commit.status(ref)
 
 = cache(cache_key, expires_in: 1.day) do
-  %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
+  %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
 
-    .table-list-cell.avatar-cell.hidden-xs
+    .avatar-cell.hidden-xs
       = author_avatar(commit, size: 36)
 
-    .table-list-cell.commit-content
-      = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
-      %span.commit-row-message.visible-xs-inline
-        &middot;
-        = commit.short_id
-      - if commit.status(ref)
-        .visible-xs-inline
-          = render_commit_status(commit, ref: ref)
-      - if commit.description?
-        %a.text-expander.hidden-xs.js-toggle-button ...
+    .commit-detail
+      .commit-content
+        = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
+        %span.commit-row-message.visible-xs-inline
+          &middot;
+          = commit.short_id
+        - if commit.status(ref)
+          .visible-xs-inline
+            = render_commit_status(commit, ref: ref)
+        - if commit.description?
+          %a.text-expander.hidden-xs.js-toggle-button ...
 
-      - if commit.description?
-        %pre.commit-row-description.js-toggle-content
-          = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
-      .commiter
-        = commit_author_link(commit, avatar: false, size: 24)
-        committed
-        #{time_ago_with_tooltip(commit.committed_date)}
+        - if commit.description?
+          %pre.commit-row-description.js-toggle-content
+            = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
+        .commiter
+          = commit_author_link(commit, avatar: false, size: 24)
+          committed
+          #{time_ago_with_tooltip(commit.committed_date)}
 
-    .table-list-cell.commit-actions.hidden-xs
-      - if commit.status(ref)
-        = render_commit_status(commit, ref: ref)
-      = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
-      = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
-      = link_to_browse_code(project, commit)
+      .commit-actions.flex-row.hidden-xs
+        - if commit.status(ref)
+          = render_commit_status(commit, ref: ref)
+        = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
+        = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+        = link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 64d93e4141c465ee20369585170605e4e6b9ec14..6f5835cb9be1c7c8ebef9ad8ff0f23f9261c2b23 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -11,4 +11,4 @@
       %li.warning-row.unstyled
         #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
   - else
-    %ul.content-list.table-list= render commits, project: @project, ref: @ref
+    %ul.content-list= render commits, project: @project, ref: @ref
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 904cdb5767f383e1b2626854755030f7394f063f..88c7d7bc44b7d611344932de6301a27a7a66cd14 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -4,7 +4,7 @@
 - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
   %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
   %li.commits-row
-    %ul.content-list.commit-list.table-list.table-wide
+    %ul.content-list.commit-list
       = render commits, project: project, ref: ref
 
 - if hidden > 0
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 80763ce67caafa1b6422b1c23e3219b59e8fc8f7..dd6797f10c028223290040d344859761740027da 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -11,14 +11,6 @@
           = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
             Commits
 
-        = nav_link(controller: %w(network)) do
-          = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
-            Network
-
-        = nav_link(controller: :compare) do
-          = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
-            Compare
-
         = nav_link(html_options: {class: branches_tab_class}) do
           = link_to namespace_project_branches_path(@project.namespace, @project) do
             Branches
@@ -26,3 +18,19 @@
         = nav_link(controller: [:tags, :releases]) do
           = link_to namespace_project_tags_path(@project.namespace, @project) do
             Tags
+
+        = nav_link(path: 'graphs#show') do
+          = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do
+            Contributors
+
+        = nav_link(controller: %w(network)) do
+          = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+            Graph
+
+        = nav_link(controller: :compare) do
+          = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
+            Compare
+
+        = nav_link(path: 'graphs#charts') do
+          = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do
+            Charts
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 30bb7412073922b35ea8ad2d6420abe8c01de6de..2f0b6e39800524b16a9a097665d409fccda2279a 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -1,7 +1,7 @@
 xml.instruct!
 xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
   xml.title   "#{@project.name}:#{@ref} commits"
-  xml.link    href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  xml.link    href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml"
   xml.link    href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
   xml.id      namespace_project_commits_url(@project.namespace, @project, @ref)
   xml.updated @commits.first.committed_date.xmlschema if @commits.any?
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 08cb8a0441312dee5a81ba3543e44d0bf5f7f755..38dbf2ac10bfbf8e22a9af4675a94a7bc1a626b3 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -2,8 +2,7 @@
 
 - page_title "Commits", @ref
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+  = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
 
 = content_for :sub_nav do
   = render "head"
@@ -27,10 +26,9 @@
       .control
         = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
           = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
-      - if current_user && current_user.private_token
-        .control
-          = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
-            = icon("rss")
+      .control
+        = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits Feed", class: 'btn' do
+          = icon("rss")
 
   %div{ id: dom_id(@project) }
     %ol#commits-list.list-unstyled.content_list
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index c8f0b547f8049326555d1aff5460016797479a0c..9007f2c24ba54be1407dc8b6227140678b96dddf 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -9,7 +9,7 @@
               Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
               To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
             %p
-              %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+              %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more
           .col-md-6.overview-image
             %span.overview-icon
               = custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 5405ff16bea71cdaca566bde81c9c10794c8caab..dd3fa814716ea390509549d3ca1c9120c1aa06c9 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,9 +1,10 @@
 - @no_container = true
 - page_title "Cycle Analytics"
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag('cycle_analytics')
 
-= render "projects/pipelines/head"
+= render "projects/head"
 
 #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
   - if @cycle_analytics_no_data
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index d1e3cb1402269d3a57a65bf90294ee92d66daf39..ec8fc4c9ee84d81862cf8bd40e084e2148ce87ee 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -18,7 +18,7 @@
     %span.key-created-at
       created #{time_ago_with_tooltip(deploy_key.created_at)}
     .visible-xs-block.visible-sm-block
-    - if @available_keys.include?(deploy_key)
+    - if @deploy_keys.key_available?(deploy_key)
       = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do
         Enable
     - else
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index c91bb9c255af73aa71f5ce2cf939b25bae19c500..1421da72418e3df1bbe6a68a4d1cea16312f68c6 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,5 +1,5 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
-  = form_errors(@key)
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
+  = form_errors(@deploy_keys.new_key)
   .form-group
     = f.label :title, class: "label-light"
     = f.text_field :title, class: 'form-control', autofocus: true, required: true
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4cfbd9add009d18d583129377dd4a1de97d3ec97
--- /dev/null
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -0,0 +1,34 @@
+.row.prepend-top-default
+  .col-lg-3.profile-settings-sidebar
+    %h4.prepend-top-0
+      Deploy Keys
+    %p
+      Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
+  .col-lg-9
+    %h5.prepend-top-0
+      Create a new deploy key for this project
+    = render @deploy_keys.form_partial_path
+  .col-lg-9.col-lg-offset-3
+    %hr
+  .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
+    %h5.prepend-top-0
+      Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size})
+    - if @deploy_keys.any_keys_enabled?
+      %ul.well-list
+        = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key
+    - else
+      .settings-message.text-center
+        No deploy keys found. Create one with the form above.
+    %h5.prepend-top-default
+      Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size})
+    - if @deploy_keys.any_available_project_keys_enabled?
+      %ul.well-list
+        = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
+    - else
+      .settings-message.text-center
+        No deploy keys from your projects could be found. Create one with the form above or add existing one below.
+    - if @deploy_keys.any_available_public_keys_enabled?
+      %h5.prepend-top-default
+        Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
+      %ul.well-list
+        = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
deleted file mode 100644
index 04fbb37d93faa2b1854636e390e1df5457ca37db..0000000000000000000000000000000000000000
--- a/app/views/projects/deploy_keys/index.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- page_title "Deploy Keys"
-
-.row.prepend-top-default
-  .col-lg-3.profile-settings-sidebar
-    %h4.prepend-top-0
-      = page_title
-    %p
-      Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
-  .col-lg-9
-    %h5.prepend-top-0
-      Create a new deploy key for this project
-    = render "form"
-  .col-lg-9.col-lg-offset-3
-    %hr
-  .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
-    %h5.prepend-top-0
-      Enabled deploy keys for this project (#{@enabled_keys.size})
-    - if @enabled_keys.any?
-      %ul.well-list
-        = render @enabled_keys
-    - else
-      .settings-message.text-center
-        No deploy keys found. Create one with the form above or add existing one below.
-    %h5.prepend-top-default
-      Deploy keys from projects you have access to (#{@available_project_keys.size})
-    - if @available_project_keys.any?
-      %ul.well-list
-        = render @available_project_keys
-    - else
-      .settings-message.text-center
-        No deploy keys from your projects could be found. Create one with the form above or add existing one below.
-    - if @available_public_keys.any?
-      %h5.prepend-top-default
-        Public deploy keys available to any project (#{@available_public_keys.size})
-      %ul.well-list
-        = render @available_public_keys
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index a680b1ca0175aca3e41f1b99cbbe71d583a919b3..506246f2ee60826b3b79693e9c8bb8a97a271330 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,9 +1,9 @@
 - if can?(current_user, :create_deployment, deployment)
   - actions = deployment.manual_actions
   - if actions.present?
-    .inline
+    .btn-group
       .dropdown
-        %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
+        %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
           = custom_icon('icon_play')
           = icon('caret-down')
         %ul.dropdown-menu.dropdown-menu-align-right
@@ -12,4 +12,3 @@
               = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
                 = custom_icon('icon_play')
                 %span= action.name.humanize
-
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index c468202569f70d5757f74f03355280e4b0c9410f..260c9023daf731d5170626ed25eda2e6511282dd 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -17,6 +17,6 @@
     #{time_ago_with_tooltip(deployment.created_at)}
 
   %td.hidden-xs
-    .pull-right
+    .pull-right.btn-group
       = render 'projects/deployments/actions', deployment: deployment
       = render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 1dbfe830d52bd09e99c8f7237d0cb00f636352d0..7d6b3701f95adaf3242de9cb6a052d75acab0a83 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -2,18 +2,21 @@
 - if defined?(blob) && blob && diff_file.submodule?
   %span
     = icon('archive fw')
-    %span
+
+    %strong.file-title-name
       = submodule_link(blob, diff_commit.id, project.repository)
+
+    = copy_file_path_button(blob.path)
 - else
   = conditional_link_to url.present?, url do
     = blob_icon diff_file.b_mode, diff_file.file_path
 
     - if diff_file.renamed_file
       - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
-      %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
+      %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
         = old_path
       &rarr;
-      %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
+      %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
         = new_path
     - else
       %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
@@ -21,7 +24,7 @@
       - if diff_file.deleted_file
         deleted
 
-  = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+  = copy_file_path_button(diff_file.new_path)
 
   - if diff_file.mode_changed?
     %small
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index cd18ba2ed00518078617c688a55bff87bc926367..c09c7b87e2460e880172f298c1e3d9f7d05c295f 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,22 +1,27 @@
 - email = local_assigns.fetch(:email, false)
 - plain = local_assigns.fetch(:plain, false)
+- discussions = local_assigns.fetch(:discussions, nil)
 - type = line.type
 - line_code = diff_file.line_code(line)
-%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
+- if discussions && !line.meta?
+  - discussion = discussions[line_code]
+%tr.line_holder{ class: type, id: (line_code unless plain) }
   - case type
   - when 'match'
     = diff_match_line line.old_pos, line.new_pos, text: line.text
-  - when 'nonewline'
+  - when 'old-nonewline', 'new-nonewline'
     %td.old_line.diff-line-num
     %td.new_line.diff-line-num
     %td.line_content.match= line.text
   - else
-    %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
+    %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } }
       - link_text = type == "new" ? " " : line.old_pos
       - if plain
         = link_text
       - else
         %a{ href: "##{line_code}", data: { linenumber: link_text } }
+      - if discussion && discussion.resolvable? && !plain
+        %diff-note-avatars{ "discussion-id" => discussion.id }
     %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
       - link_text = type == "old" ? " " : line.new_pos
       - if plain
@@ -29,9 +34,6 @@
       - else
         = diff_line_content(line.text)
 
-- discussions = local_assigns.fetch(:discussions, nil)
-- if discussions && !line.meta?
-  - discussion = discussions[line_code]
-  - if discussion
-    - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
-    = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
+- if discussion
+  - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
+  = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 997bf0fc5607c11b03e952fb5dc2cf68ab0f2635..b7346f27ddbf8476a8fcd133de647eb6b0b73679 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -4,19 +4,24 @@
     - diff_file.parallel_diff_lines.each do |line|
       - left = line[:left]
       - right = line[:right]
+      - last_line = right.new_pos if right
+      - unless @diff_notes_disabled
+        - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
       %tr.line_holder.parallel
         - if left
           - case left.type
           - when 'match'
             = diff_match_line left.old_pos, nil, text: left.text, view: :parallel
-          - when 'nonewline'
+          - when 'old-nonewline', 'new-nonewline'
             %td.old_line.diff-line-num
             %td.line_content.match= left.text
           - else
             - left_line_code = diff_file.line_code(left)
             - left_position = diff_file.position(left)
-            %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+            %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
               %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
+              - if discussion_left && discussion_left.resolvable?
+                %diff-note-avatars{ "discussion-id" => discussion_left.id }
             %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
         - else
           %td.old_line.diff-line-num.empty-cell
@@ -26,23 +31,23 @@
           - case right.type
           - when 'match'
             = diff_match_line nil, right.new_pos, text: left.text, view: :parallel
-          - when 'nonewline'
+          - when 'old-nonewline', 'new-nonewline'
             %td.new_line.diff-line-num
             %td.line_content.match= right.text
           - else
             - right_line_code = diff_file.line_code(right)
             - right_position = diff_file.position(right)
-            %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+            %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
               %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
+              - if discussion_right && discussion_right.resolvable?
+                %diff-note-avatars{ "discussion-id" => discussion_right.id }
             %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
         - else
           %td.old_line.diff-line-num.empty-cell
           %td.line_content.parallel
 
-      - unless @diff_notes_disabled
-        - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
-        - if discussion_left || discussion_right
-          = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+      - if discussion_left || discussion_right
+        = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
     - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
       - last_line = diff_file.diff_lines.last
       - if last_line.new_pos < total_lines
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b9300efd04f2ac26f87b33885a79b05d11962316..2802a4eca7bc5c38477e0cbb0d9fe7269a8995af 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,3 +1,4 @@
+= render "projects/settings/head"
 .project-edit-container
   .row.prepend-top-default
     .col-lg-3.profile-settings-sidebar
@@ -120,7 +121,7 @@
           .form-group
             - if @project.avatar?
               .avatar-container.s160
-                = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+                = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160')
             %p.light
               - if @project.avatar_in_git
                 Project avatar in repository: #{ @project.avatar_in_git }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 58c085cdb9d0a49d1de1b2ac38ea7ab487f77bdd..85e442e115c423a40a583ef35a930286adef760a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -5,6 +5,7 @@
     = render 'shared/no_ssh'
     = render 'shared/no_password'
 
+= render "projects/head"
 = render "home_panel"
 
 .row-content-block.second-block.center
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index 4c8fe1c271b6d6cf7dec025673647d10cda0b46c..bf0f181907339e129ece9321181a9420b209e71f 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,3 @@
 - if environment.external_url && can?(current_user, :read_environment, environment)
-  = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+  = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
     = icon('external-link')
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..acbac1869fdb7fee2e1f7ffd6c7ac3f4ea0e48fe
--- /dev/null
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -0,0 +1,6 @@
+- environment = local_assigns.fetch(:environment)
+
+- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
+
+= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
+  = icon('area-chart')
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index d9cb7bc033101bfeeae3cdf2fbf0cef55ccccb8a..4b101447bc0c64f46faeae94c45986bafb918ff7 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -3,6 +3,7 @@
 = render "projects/pipelines/head"
 
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag("environments_folder")
 
 #environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 1f27d41ddd90f1efa913ec5b0ae450085799e787..80d2b6f5d9505b03d95d8f837fb5635e962832ef 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,6 +3,7 @@
 = render "projects/pipelines/head"
 
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag("environments")
 
 #environments-list-view{ data: { environments_data: environments_list_data,
@@ -13,7 +14,4 @@
   "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
   "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
   "help-page-path" => help_page_path("ci/environments"),
-  "css-class" => container_class,
-  "commit-icon-svg" => custom_icon("icon_commit"),
-  "terminal-icon-svg" => custom_icon("icon_terminal"),
-  "play-icon-svg" => custom_icon("icon_play") } }
+  "css-class" => container_class } }
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b8c1782f050ec8b952ab05192d3aca77509ceb34
--- /dev/null
+++ b/app/views/projects/environments/metrics.html.haml
@@ -0,0 +1,24 @@
+- @no_container = true
+- page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_d3')
+  = page_specific_javascript_bundle_tag('monitoring')
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+  .top-area
+    .row
+      .col-sm-6
+        %h3.page-title
+          Environment:
+          = @environment.name
+
+      .col-sm-6
+        .nav-controls
+          = render 'projects/deployments/actions', deployment: @environment.last_deployment
+  .row
+    .col-sm-12
+      %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+  .row
+    .col-sm-12
+      %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7036325fff809c70589dce6137ad4bee4795778b..f463a429f65943a609f29396aab74b14dd0cd0b4 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,6 +8,7 @@
       %h3.page-title= @environment.name
     .col-md-3
       .nav-controls
+        = render 'projects/environments/metrics_button', environment: @environment
         = render 'projects/environments/terminal_button', environment: @environment
         = render 'projects/environments/external_url', environment: @environment
         - if can?(current_user, :update_environment, @environment)
@@ -15,7 +16,7 @@
         - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
           = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
 
-  .deployments-container
+  .environments-container
     - if @deployments.blank?
       .blank-state.blank-state-no-icon
         %h2.blank-state-title
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
deleted file mode 100644
index 67018aaa2acee5a828264bbdc17c884fa46a467e..0000000000000000000000000000000000000000
--- a/app/views/projects/graphs/_head.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-= content_for :sub_nav do
-  .scrolling-tabs-container.sub-nav-scroll
-    = render 'shared/nav_scroll'
-    .nav-links.sub-nav.scrolling-tabs
-      %ul{ class: (container_class) }
-
-        - content_for :page_specific_javascripts do
-          = page_specific_javascript_bundle_tag('lib_chart')
-          = page_specific_javascript_bundle_tag('graphs')
-        = nav_link(action: :show) do
-          = link_to 'Contributors', namespace_project_graph_path
-        = nav_link(action: :commits) do
-          = link_to 'Commits', commits_namespace_project_graph_path
-        = nav_link(action: :languages) do
-          = link_to 'Languages', languages_namespace_project_graph_path
-        - if @project.feature_available?(:builds, current_user)
-          = nav_link(action: :ci) do
-            = link_to ci_namespace_project_graph_path do
-              Continuous Integration
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/charts.html.haml
similarity index 53%
rename from app/views/projects/graphs/commits.html.haml
rename to app/views/projects/graphs/charts.html.haml
index c8a82f7bca39300e77c89057c674683fd3303761..464ac34d96143472783066b9f6b0edc4686b1d20 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -1,38 +1,58 @@
 - @no_container = true
-- page_title "Commits", "Graphs"
-= render 'head'
+- page_title "Charts"
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_d3')
+  = page_specific_javascript_bundle_tag('graphs')
+= render "projects/commits/head"
 
-%div{ class: container_class }
-  .sub-header-block
-    .tree-ref-holder
-      = render 'shared/ref_switcher', destination: 'graphs_commits'
-    %ul.breadcrumb.repo-breadcrumb
-      = commits_breadcrumbs
+.repo-charts{ class: container_class }
+  %h4.sub-header
+    Programming languages used in this repository
 
-  %p.lead
-    Commit statistics for
-    %strong= @ref
-    #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+  .row
+    .col-md-4
+      %ul.bordered-list
+        - @languages.each do |language|
+          %li
+            %span{ style: "color: #{language[:color]}" }
+              = icon('circle')
+            &nbsp;
+            = language[:label]
+            .pull-right
+              = language[:value]
+              \%
+    .col-md-8
+      %canvas#languages-chart{ height: 400 }
+
+.repo-charts{ class: container_class }
+  .sub-header-block.border-top
+
+  .row.tree-ref-header
+    .col-md-6
+      %h4
+        Commit statistics for
+        %strong= @ref
+        #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+
+    .col-md-6
+      .tree-ref-container
+        .tree-ref-holder
+          = render 'shared/ref_switcher', destination: 'graphs_commits'
+        %ul.breadcrumb.repo-breadcrumb
+          = commits_breadcrumbs
 
   .row
     .col-md-6
-      %ul
+      %ul.commit-stats
         %li
-          %p.lead
-            %strong= @commits_graph.commits.size
-            commits during
-            %strong= @commits_graph.duration
-            days
+          Total:
+          %strong #{@commits_graph.commits.size} commits
         %li
-          %p.lead
-            Average
-            %strong= @commits_graph.commit_per_day
-            commits per day
+          Average per day:
+          %strong #{@commits_graph.commit_per_day} commits
         %li
-          %p.lead
-            Contributed by
-            %strong= @commits_graph.authors
-            authors
+          Authors:
+          %strong= @commits_graph.authors
     .col-md-6
       %div
         %p.slead
@@ -40,15 +60,18 @@
         %canvas#month-chart
   .row
     .col-md-6
-      %div
-        %p.slead
-          Commits per day hour (UTC)
-        %canvas#hour-chart
     .col-md-6
       %div
         %p.slead
           Commits per weekday
         %canvas#weekday-chart
+  .row
+    .col-md-6
+    .col-md-6
+      %div
+        %p.slead
+          Commits per day hour (UTC)
+        %canvas#hour-chart
 
 :javascript
   var responsiveChart = function (selector, data) {
@@ -93,3 +116,12 @@
 
   var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json});
   responsiveChart($('#month-chart'), monthData);
+
+  var data = #{@languages.to_json};
+  var ctx = $("#languages-chart").get(0).getContext("2d");
+  var options = {
+    scaleOverlay: true,
+    responsive: true,
+    maintainAspectRatio: false
+  }
+  var myPieChart = new Chart(ctx).Pie(data, options);
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
deleted file mode 100644
index 6be4273b6abbdae6aa19c0dcfc5516c2ece29943..0000000000000000000000000000000000000000
--- a/app/views/projects/graphs/ci.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- @no_container = true
-- page_title "Continuous Integration", "Graphs"
-= render 'head'
-
-%div{ class: container_class }
-  .sub-header-block
-    .oneline
-      A collection of graphs for Continuous Integration
-
-  #charts.ci-charts
-    .row
-      .col-md-6
-        = render 'projects/graphs/ci/overall'
-      .col-md-6
-        = render 'projects/graphs/ci/build_times'
-
-    %hr
-    = render 'projects/graphs/ci/builds'
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
deleted file mode 100644
index fcfcae0be2098df40bd7c76e10e2fd4906faef3c..0000000000000000000000000000000000000000
--- a/app/views/projects/graphs/languages.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-- @no_container = true
-- page_title "Languages", "Graphs"
-= render 'head'
-
-%div{ class: container_class }
-  .sub-header-block
-    .oneline
-      Programming languages used in this repository
-
-  .row
-    .col-md-8
-      %canvas#languages-chart{ height: 400 }
-    .col-md-4
-      %ul.bordered-list
-        - @languages.each do |language|
-          %li
-            %span{ style: "color: #{language[:color]}" }
-              = icon('circle')
-            &nbsp;
-            = language[:label]
-            .pull-right
-              = language[:value]
-              \%
-
-:javascript
-  var data = #{@languages.to_json};
-  var ctx = $("#languages-chart").get(0).getContext("2d");
-  var options = {
-    scaleOverlay: true,
-    responsive: true,
-    maintainAspectRatio: false
-  }
-  var myPieChart = new Chart(ctx).Pie(data, options);
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 5ebb939a109ca69fc6c781c505c0f661f1e013a4..680f8ae6c8f9b32aafb683757e1561210e37340d 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,6 +1,9 @@
 - @no_container = true
-- page_title "Contributors", "Graphs"
-= render 'head'
+- page_title "Contributors"
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_d3')
+  = page_specific_javascript_bundle_tag('graphs')
+= render 'projects/commits/head'
 
 %div{ class: container_class }
   .sub-header-block
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 7076f5db015fe93af4e8994fcbbc694b8986c87b..8b011af78eb8546ad209229c9c1bbdedc9cfd96d 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,8 +1,2 @@
 = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
   = render 'shared/issuable/form', f: f, issuable: @issue
-
-:javascript
-  $('.assign-to-me-link').on('click', function(e){
-    $('#issue_assignee_id').val("#{current_user.id}").trigger("change");
-    e.preventDefault();
-  });
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 4825820c4d9d7ad586b80173dae0c14a910adc30..7a188cb64459cd3e657088b7cef14c062f014e8c 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -7,7 +7,7 @@
           = nav_link(controller: :issues) do
             = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
               %span
-                Issues
+                List
 
           = nav_link(controller: :boards) do
             = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index a0df0db77c5cecc6e5fbc0c6488df3724e08b390..4feec09bb5d490cde12410a7fae8161dd01ac1f6 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
   xml.link    href: url_for(params), rel: "self", type: "application/atom+xml"
   xml.link    href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
   xml.id      namespace_project_issues_url(@project.namespace, @project)
-  xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+  xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
 
   xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
 end
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 8ea1a3a45e18a2df3ca8239cd86231b5e1238fcd..f3a429d12d94ae0ddb54ba4ac2f62bc6bbfcd158 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -10,26 +10,23 @@
   = page_specific_javascript_bundle_tag('filtered_search')
 
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
+  = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
 
 - if project_issues(@project).exists?
   %div{ class: (container_class) }
     .top-area
       = render 'shared/issuable/nav', type: :issues
       .nav-controls
-        - if current_user
-          = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
-            = icon('rss')
-        - if can? current_user, :create_issue, @project
-          = link_to new_namespace_project_issue_path(@project.namespace,
-                                                     @project,
-                                                     issue: { assignee_id: issues_finder.assignee.try(:id),
-                                                              milestone_id: issues_finder.milestones.first.try(:id) }),
-                                                     class: "btn btn-new",
-                                                     title: "New Issue",
-                                                     id: "new_issue_link" do
-            New Issue
+        = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
+          = icon('rss')
+        = link_to new_namespace_project_issue_path(@project.namespace,
+                                                   @project,
+                                                   issue: { assignee_id: issues_finder.assignee.try(:id),
+                                                            milestone_id: issues_finder.milestones.first.try(:id) }),
+                                                   class: "btn btn-new",
+                                                   title: "New Issue",
+                                                   id: "new_issue_link" do
+          New Issue
     = render 'shared/issuable/search_bar', type: :issues
 
     .issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 069f3d979435edfe6b18988eec081948099ef8fc..6ac05bf3afee6e62486400c948a0cfb8ccd9af04 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,8 +2,6 @@
 - page_title           "#{@issue.title} (#{@issue.to_reference})", "Issues"
 - page_description     @issue.description
 - page_card_attributes @issue.card_attributes
-- content_for :page_specific_javascripts do
-  = page_specific_javascript_bundle_tag('lib_vue')
 
 .clearfix.detail-page-header
   .issuable-header
@@ -22,37 +20,34 @@
       = confidential_icon(@issue)
       = issuable_meta(@issue, @project, "Issue")
 
-  - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
-    .issuable-actions
-      .clearfix.issue-btn-group.dropdown
-        %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
-          Options
-          = icon('caret-down')
-        .dropdown-menu.dropdown-menu-align-right.hidden-lg
-          %ul
-            - if can?(current_user, :create_issue, @project)
-              %li
-                = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
-            - if can?(current_user, :update_issue, @issue)
-              %li
-                = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
-              %li
-                = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-              %li
-                = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
-            - if @issue.submittable_as_spam_by?(current_user)
-              %li
-                = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
-        - if can?(current_user, :create_issue, @project)
-          = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
-            New issue
-        - if can?(current_user, :update_issue, @issue)
-          = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
-          = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+  .issuable-actions
+    .clearfix.issue-btn-group.dropdown
+      %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+        Options
+        = icon('caret-down')
+      .dropdown-menu.dropdown-menu-align-right.hidden-lg
+        %ul
+          %li
+            = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+          - if can?(current_user, :update_issue, @issue)
+            %li
+              = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+            %li
+              = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+            %li
+              = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
           - if @issue.submittable_as_spam_by?(current_user)
-            = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
-          = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
+            %li
+              = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+    = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+      New issue
+    - if can?(current_user, :update_issue, @issue)
+      = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+      = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+      - if @issue.submittable_as_spam_by?(current_user)
+        = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+      = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
 
 
 .issue-details.issuable-details
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
index 09aa401e44ab55dd48283b2627d15e10527ba8a4..6da7c317f3a47c0efbffd670b3f764bd32fc0814 100644
--- a/app/views/projects/issues/verify.html.haml
+++ b/app/views/projects/issues/verify.html.haml
@@ -1,4 +1,5 @@
 - form = [@project.namespace.becomes(Namespace), @project, @issue]
 
 = render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
-  = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+  = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
+  = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index a80f9aa4c4a87de70a58a7394bf12b1607053574..04bd4e8b6830e8f18c392dc67254f9bcc07c83dc 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -2,16 +2,15 @@
   This service will be installed on the Mattermost instance at
   %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
 %hr
-= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f|
   %h4 Team
   %p
     = @teams.one? ? 'The team' : 'Select the team'
     where the slash commands will be used in
-  - selected_id = @teams.one? ? @teams.keys.first : 0
-  - options = mattermost_teams_options(@teams)
-  - options = options_for_select(options, selected_id)
-  = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
-  = f.hidden_field(:team_id, value: selected_id) if @teams.one?
+  - selected_id = @teams.one? ? @teams.first['id'] : nil
+  - options = options_for_select(mattermost_teams_options(@teams), selected_id)
+  = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
+  = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
   .help-block
     - if @teams.one?
       This is the only available team.
@@ -25,7 +24,7 @@
   %hr
   %h4 Command trigger word
   %p Choose the word that will trigger commands
-  = f.text_field(:trigger, value: @project.path, class: 'form-control')
+  = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
   .help-block
     %p
       Trigger word must be unique, and can't begin with a slash or contain any spaces.
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 96b1d2aee61dd72fdaf213af327d565bc29ed59d..15829a3f143548c3bc8f0bc6a11bc12c584a642f 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,3 +1,5 @@
+- @body_class = 'card-content'
+
 .service-installation
   .inline.pull-right
     = custom_icon('mattermost_logo', size: 48)
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 88525f4036a7ac2f13f1124d6b82c54991e05d3d..9607a7b5d064ca5dc88a0c5f76563cb35fe97f39 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,8 +1,2 @@
 = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
   = render 'shared/issuable/form', f: f, issuable: @merge_request
-
-:javascript
-  $('.assign-to-me-link').on('click', function(e){
-    $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
-    e.preventDefault();
-  });
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 466ec1475d8574d4a0e1be716538416345696789..ad14b4e583e7b78a4e1419deb4f7cf53cede8be0 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
                   selected: f.object.source_project_id
           .merge-request-select.dropdown
             = f.hidden_field :source_branch
-            = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+            = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
             .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
               = dropdown_title("Select source branch")
               = dropdown_filter("Search branches")
@@ -30,7 +30,7 @@
                   branches: @merge_request.source_branches,
                   selected: f.object.source_branch
         .panel-footer
-          = icon('spinner spin', class: 'js-source-loading')
+          .text-center= icon('spinner spin', class: 'js-source-loading')
           %ul.list-unstyled.mr_source_commit
 
     .col-md-6
@@ -60,7 +60,7 @@
                   branches: @merge_request.target_branches,
                   selected: f.object.target_branch
         .panel-footer
-          = icon('spinner spin', class: "js-target-loading")
+          .text-center= icon('spinner spin', class: "js-target-loading")
           %ul.list-unstyled.mr_target_commit
 
   - if @merge_request.errors.any?
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index bd72310c16b528b7de8025d190a5cbf05f069cda..e7fcac4c477376b63353fc7ba2aa171ece3c8aee 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -51,11 +51,6 @@
   .mr-loading-status
     = spinner
 
-:javascript
-  $('.assign-to-me-link').on('click', function(e){
-    $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
-    e.preventDefault();
-  });
 :javascript
   var merge_request = new MergeRequest({
     action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 521b0694ca9d655bc5002e8c8f640970d3a45e46..6682a85ffa63ff15b8cdac14005a0214f4844ce2 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,6 +3,7 @@
 - page_description     @merge_request.description
 - page_card_attributes @merge_request.card_attributes
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag('diff_notes')
 
 .merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
@@ -15,7 +16,7 @@
         .pull-right
           - if @merge_request.source_branch_exists?
             - if koding_enabled? && @repository.koding_yml
-              = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+              = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
                 Run in IDE (Koding)
             = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
               Check out branch
@@ -28,9 +29,9 @@
               %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
               %li= link_to "Plain Diff",    merge_request_path(@merge_request, format: :diff)
       .normal
-        %span Request to merge
+        %span <b>Request to merge</b>
         %span.label-branch= source_branch_with_namespace(@merge_request)
-        %span into
+        %span <b>into</b>
         %span.label-branch
           = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
         - if @merge_request.open? && @merge_request.diverged_from_target_branch?
@@ -81,6 +82,7 @@
                     = render "shared/icons/icon_status_success.svg"
                   %span.line-resolve-text
                     {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+                = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
                 = render "discussions/jump_to_next"
 
     .tab-content#diff-notes-app
diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
similarity index 100%
rename from app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml
rename to app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index 1ecd9924d886b1344be5682f8e402dd5a9e1045e..51d59280be826b76ad900ee0ffbcfbcfd8d32660 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,6 +1,6 @@
 - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_bundle_tag('lib_vue')
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag('merge_conflicts')
   = page_specific_javascript_tag('lib/ace.js')
 = render "projects/merge_requests/show/mr_title"
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 83e6c026ba7deb2da3c7e7bade1d13e3c003ab5f..8a96c8dacf6c17819c0f1b6ba3b78218962e7398 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,7 +2,6 @@
 - @bulk_edit = can?(current_user, :admin_merge_request, @project)
 
 - page_title "Merge Requests"
-= render "projects/issues/head"
 = render 'projects/last_push'
 
 - content_for :page_specific_javascripts do
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index 84b6c9ebc5cf1499d7eee272fa0aadd2b4ab5309..f0a23bec5e7d2428336dd91b9497d40e9af00d62 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -2,9 +2,9 @@
 - when :success
   :plain
     merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
-- when :merge_when_build_succeeds
+- when :merge_when_pipeline_succeeds
   :plain
-    $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}");
+    $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
 - when :sha_mismatch
   :plain
     $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 93ed4b68e0e83a8b73e705a8db65cd588e5fc07e..cde0ce08e1440fe3a14c2f1009021e1e6b95b838 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
           %strong Tip:
           = succeed '.' do
             You can also checkout merge requests locally by
-            = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
+            = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
 
 :javascript
   $(function(){
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index c676953f729ab79cfd2a28e54cfd0d396a9f6abb..1298376ac2502bbce8d6cc3ff092373842375664 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,8 +1,8 @@
 - if @pipeline
   .mr-widget-heading
-    - %w[success success_with_warnings skipped canceled failed running pending].each do |status|
+    - %w[success success_with_warnings skipped manual canceled failed running pending].each do |status|
       .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
-        %div{ class: "ci-status-icon-#{status}" }
+        %div{ class: "ci-status-icon ci-status-icon-#{status}" }
           = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
             = ci_icon_for_status(status)
         %span
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 7794d6d7df2ffadd052c924af87fed2626663b9d..adc3bbc37f307f7169e0827433c396ec5975e05a 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -7,28 +7,46 @@
         by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
         #{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
     - if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
-      %p
-        The changes were merged into
-        #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
-        The source branch has been removed.
+      .remove-message-pipes
+        %ul
+          %li
+            %span
+              The changes were merged into
+              #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+          %li
+            %span
+              The source branch has been removed.
       = render 'projects/merge_requests/widget/merged_buttons'
     - elsif @merge_request.can_remove_source_branch?(current_user)
-      .remove_source_branch_widget
-        %p
-          The changes were merged into
-          #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
-          You can remove the source branch now.
+      .remove_source_branch_widget.remove-message-pipes
+        %ul
+          %li
+            %span
+              The changes were merged into
+              #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+          %li
+            %span
+              You can remove the source branch now.
         = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
-      .remove_source_branch_widget.failed.hide
-        %p
-          Failed to remove source branch '#{@merge_request.source_branch}'.
-
-      .remove_source_branch_in_progress.hide
-        %p
-          = icon('spinner spin')
-          Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
+      .remove_source_branch_widget.failed.remove-message-pipes.hide
+        %ul
+          %li
+            %span
+              Failed to remove source branch '#{@merge_request.source_branch}'.
+      .remove_source_branch_in_progress.remove-message-pipes.hide
+        %ul
+          %li
+            %span
+              = icon('spinner spin')
+              Removing source branch '#{@merge_request.source_branch}'.
+          %li
+            %span
+              Please wait, this page will be automatically reloaded.
     - else
-      %p
-        The changes were merged into
-        #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
-      = render 'projects/merge_requests/widget/merged_buttons'
+      .remove-message-pipes
+        %ul
+          %li
+            %span
+              The changes were merged into
+              #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+        = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 9eef011b5910eef92d54a4489d94a08808b1981b..caf3bf54eef2c658c3340f6229f695992ed3d057 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -9,6 +9,6 @@
         = icon('trash-o')
         Remove Source Branch
     - if mr_can_be_reverted
-      = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
+      = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close")
     - if mr_can_be_cherry_picked
       = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index c0d6ab669b812602a37395865a8559fa6168f26b..bc426f1dc0c022d8c279b80df32b6a52e0e6b3b3 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -19,14 +19,16 @@
       = render 'projects/merge_requests/widget/open/conflicts'
     - elsif @merge_request.work_in_progress?
       = render 'projects/merge_requests/widget/open/wip'
-    - elsif @merge_request.merge_when_build_succeeds?
-      = render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
+    - elsif @merge_request.merge_when_pipeline_succeeds?
+      = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
     - elsif !@merge_request.can_be_merged_by?(current_user)
       = render 'projects/merge_requests/widget/open/not_allowed'
     - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?)
       = render 'projects/merge_requests/widget/open/build_failed'
     - elsif !@merge_request.mergeable_discussions_state?
       = render 'projects/merge_requests/widget/open/unresolved_discussions'
+    - elsif @pipeline&.blocked?
+      = render 'projects/merge_requests/widget/open/manual'
     - elsif @merge_request.can_be_merged? || resolved_conflicts
       = render 'projects/merge_requests/widget/open/accept'
 
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 b730ced4214e099d7e71a1f8aa7a0e4904427456..c94c7944c0b5dcd845e5a283d92e739fef80791f 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,8 +1,6 @@
 - content_for :page_specific_javascripts do
   = page_specific_javascript_bundle_tag('merge_request_widget')
 
-- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
-
 = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
   = hidden_field_tag :authenticity_token, form_authenticity_token
   = hidden_field_tag :sha, @merge_request.diff_head_sha
@@ -11,24 +9,24 @@
       .accept-action
         - if @pipeline && @pipeline.active?
           %span.btn-group
-            = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
+            = button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do
               Merge When Pipeline Succeeds
-            - unless @project.only_allow_merge_if_build_succeeds?
-              = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
+            - unless @project.only_allow_merge_if_pipeline_succeeds?
+              = button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do
                 = icon('caret-down')
                 %span.sr-only
                   Select Merge Moment
               %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
                 %li
-                  = link_to "#", class: "merge_when_build_succeeds" do
+                  = link_to "#", class: "merge_when_pipeline_succeeds" do
                     = icon('check fw')
                     Merge When Pipeline Succeeds
                 %li
-                  = link_to "#", class: "accept_merge_request" do
+                  = link_to "#", class: "accept-merge-request" do
                     = icon('warning fw')
                     Merge Immediately
         - else
-          = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do
+          = f.button class: "btn btn-grouped js-merge-button accept-merge-request" do
             Accept Merge Request
       - if @merge_request.force_remove_source_branch?
         .accept-control
@@ -49,4 +47,4 @@
           text: @merge_request.merge_commit_message,
           rows: 14, hint: true
 
-    = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
+    = hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off"
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index c98b2c425977f740b907e61b6864032b5ee0eb42..621ee3130264c55728d57bc859b38db7c17874c5 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -3,20 +3,24 @@
 - can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
 
 %h4.has-conflicts
-  = icon("exclamation-triangle")
-  This merge request contains merge conflicts
+  %p
+    = icon("exclamation-triangle")
+    This merge request contains merge conflicts
 
-%p
-  To merge this request, resolve these conflicts
-  - if can_resolve && !can_resolve_in_ui
-    locally
-  or
-  - unless can_merge
-    ask someone with write access to this repository to
-  merge it locally.
+.remove-message-pipes
+  %ul
+    %li
+      %span
+        To merge this request, resolve these conflicts
+        - if can_resolve && !can_resolve_in_ui
+          locally
+        or
+        - unless can_merge
+          ask someone with write access to this repository to
+        merge it locally.
 
 - if (can_resolve && can_resolve_in_ui) || can_merge
-  .btn-group
+  .merged-buttons.clearfix
     - if can_resolve && can_resolve_in_ui
       = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
     - if can_merge
diff --git a/app/views/projects/merge_requests/widget/open/_manual.html.haml b/app/views/projects/merge_requests/widget/open/_manual.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..9078b7e21dd9ccbed90b31c742b1d89254cf39e3
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_manual.html.haml
@@ -0,0 +1,4 @@
+%h4
+  Pipeline blocked
+%p
+  The pipeline for this merge request requires a manual action to proceed.
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml
similarity index 57%
rename from app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
rename to app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml
index cf7abf3756c7aa6c70e7cb36a43986581818f8f7..5f347acce4d7d6f50ab83654b086653ad7ea358b 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml
@@ -4,18 +4,23 @@
 %h4
   Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
   to be merged automatically when the pipeline succeeds.
-%div
-  %p
-    = succeed '.' do
-      The changes will be merged into
-      %span.label-branch= @merge_request.target_branch
-    - if @merge_request.remove_source_branch?
-      The source branch will be removed.
-    - else
-      The source branch will not be removed.
+.remove-message-pipes
+  %ul
+    %li
+      %span
+        = succeed '.' do
+          The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}
+        - if @merge_request.remove_source_branch?
+          %li
+            %span
+              The source branch will be removed.
+        - else
+          %li
+            %span
+              The source branch will not be removed.
 
   - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
-  - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+  - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
   - if remove_source_branch_button || user_can_cancel_automatic_merge
     .clearfix.prepend-top-10
       - if remove_source_branch_button
@@ -24,5 +29,5 @@
           Remove Source Branch When Merged
 
       - if user_can_cancel_automatic_merge
-        = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
+        = link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
           Cancel Automatic Merge
diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
index e094f97f3b68d4f3e1a761cdc0f43674cbe1c21d..ec9346ce89b08efcd717a6a9ba471ed3c671ffef 100644
--- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -6,5 +6,5 @@
   Please resolve these discussions
   - if @project.issues_enabled? && can?(current_user, :create_issue, @project)
     or
-    = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid)
+    = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: @merge_request.iid)
   to allow this merge request to be merged.
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 11f41e75e633750f444f99fd9ed3578ec71e89c5..55b0b837c6d438ace95aac72c8f74eb8dc5fe3a1 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -5,7 +5,7 @@
 %div{ class: container_class }
 
   %h3.page-title
-    Edit Milestone ##{@milestone.iid}
+    Edit Milestone #{@milestone.to_reference}
 
   %hr
 
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index ad2bfbec915d57387d843b5e38c398925faccbbf..918f5d161bbb799525932427d19472d0bac9feca 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,14 +1,14 @@
 - @no_container = true
-- page_title "Milestones"
-= render "projects/issues/head"
+- page_title 'Milestones'
+= render 'projects/issues/head'
 
 %div{ class: container_class }
   .top-area
-    = render 'shared/milestones_filter'
+    = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
 
     .nav-controls
       - if can?(current_user, :admin_milestone, @project)
-        = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+        = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
           New Milestone
 
   .milestones
@@ -19,4 +19,4 @@
         %li
           .nothing-here-block No milestones to show
 
-    = paginate @milestones, theme: "gitlab"
+    = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 06a31698ee6d7e68ff51336ae0b7338b7d36f1df..d16f49bd33a8761ca13cbe1b27a7bd2c2fb73390 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -19,10 +19,9 @@
         Open
     .header-text-content
       %span.identifier
-        Milestone ##{@milestone.iid}
+        %strong
+          Milestone #{@milestone.to_reference}
       - if @milestone.due_date || @milestone.start_date
-        %span.creator
-          &middot;
         = milestone_date_range(@milestone)
     .milestone-buttons
       - if can?(current_user, :admin_milestone, @project)
@@ -47,7 +46,7 @@
             = preserve do
               = markdown_field(@milestone, :description)
 
-  - if @milestone.total_items_count(current_user).zero?
+  - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
     .alert.alert-success.prepend-top-default
       %span Assign some issues to this milestone.
   - elsif @milestone.complete?(current_user) && @milestone.active?
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b88eef65cef2c03d43501d6405b588d79dd1cf7d..ed6077f6c6bf208da7cadf20fe6cccc82e7a3d5f 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,6 +1,5 @@
-- page_title "Network", @ref
+- page_title "Graph", @ref
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_tag('lib/raphael.js')
   = page_specific_javascript_bundle_tag('network')
 = render "projects/commits/head"
 = render "head"
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 2a98bba05eefc67802037339fa6ecdc12a698ea6..d129da943f8519c0f49267541b553ba732d7e566 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,5 +1,6 @@
 - page_title    'New Project'
 - header_title  "Projects", dashboard_projects_path
+- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
 
 .project-edit-container
   .project-edit-errors
@@ -95,7 +96,7 @@
           = f.label :visibility_level, class: 'label-light' do
             Visibility Level
             = link_to icon('question-circle'), help_page_path("public_access/public_access")
-          = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
+          = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
 
         = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
         = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index a73e8f345e07872056c6792c2e953a476312419a..5552086bc5007b52261944dc27408d0853e1950f 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,7 +2,7 @@
 - return if note.cross_reference_not_visible_for?(current_user)
 
 - note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
+%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
   .timeline-entry-inner
     .timeline-icon
       %a{ href: user_path(note.author) }
@@ -30,27 +30,30 @@
 
             - if note.resolvable?
               - can_resolve = can?(current_user, :resolve_note, note)
-              %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
+              %resolve-btn{ "project-path" => project_path(note.project),
+                  "discussion-id" => note.discussion_id,
                   ":note-id" => note.id,
                   ":resolved" => note.resolved?,
                   ":can-resolve" => can_resolve,
-                  "resolved-by" => "#{note.resolved_by.try(:name)}",
+                  ":author-name" => "'#{j(note.author.name)}'",
+                  "author-avatar" => note.author.avatar_url,
+                  ":note-truncated" => "'#{truncate(note.note, length: 17)}'",
+                  ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
                   "v-show" => "#{can_resolve || note.resolved?}",
                   "inline-template" => true,
                   "ref" => "note_#{note.id}" }
 
-                .note-action-button
+                %button.note-action-button.line-resolve-btn{ type: "button",
+                    class: ("is-disabled" unless can_resolve),
+                    ":class" => "{ 'is-active': isResolved }",
+                    ":aria-label" => "buttonText",
+                    "@click" => "resolve",
+                    ":title" => "buttonText",
+                    "v-show" => "!loading",
+                    ":ref" => "'button'" }
                   = icon("spin spinner", "v-show" => "loading")
-                  %button.line-resolve-btn{ type: "button",
-                      class: ("is-disabled" unless can_resolve),
-                      ":class" => "{ 'is-active': isResolved }",
-                      ":aria-label" => "buttonText",
-                      "@click" => "resolve",
-                      ":title" => "buttonText",
-                      "v-show" => "!loading",
-                      ":ref" => "'button'" }
 
-                    = render "shared/icons/icon_status_success.svg"
+                  = render "shared/icons/icon_status_success.svg"
 
             - if current_user
               - if note.emoji_awardable?
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 08c73d94a0997172e79a48a15820ba4892223d51..90a150aa74c7941f8e486a7606dc0ebb8d122673 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -23,4 +23,4 @@
           to post a comment
 
 :javascript
-  var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+  var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
index 9db46f0b1fca66e27fe001c49557484bce3af1dc..e442e6e9a09d653cabc6a28e82bee5171c2f408f 100644
--- a/app/views/projects/pages/_use.html.haml
+++ b/app/views/projects/pages/_use.html.haml
@@ -5,4 +5,6 @@
     .panel-body
       %p
         Learn how to upload your static site and have it served by
-        GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
+        GitLab by following the
+        = succeed '.' do
+          = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank'
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index b6595269b06cbc07404e555820e02b664683bf40..259d5bd63d61a361f59b38f1169bf328b3cc4d93 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,4 +1,6 @@
 - page_title 'Pages'
+= render "projects/settings/head"
+
 %h3.page_title
   Pages
 
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index 721a9b6beb51556cd3e76f0a8d6959d6bd615938..a5acb7ac4a5b223213dce289c0671864bab5a992 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -4,25 +4,25 @@
     .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
       %ul{ class: (container_class) }
         - if project_nav_tab? :pipelines
-          = nav_link(controller: :pipelines) do
+          = nav_link(path: 'pipelines#index', controller: :pipelines) do
             = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
               %span
                 Pipelines
 
         - if project_nav_tab? :builds
-          = nav_link(controller: %w(builds)) do
+          = nav_link(path: 'builds#index', controller: :builds) do
             = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
               %span
                 Jobs
 
         - if project_nav_tab? :environments
-          = nav_link(controller: %w(environments)) do
+          = nav_link(path: 'environments#index', controller: :environments) do
             = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
               %span
                 Environments
 
-        - if can?(current_user, :read_cycle_analytics, @project)
-          = nav_link(controller: %w(cycle_analytics)) do
-            = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+        - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
+          = nav_link(path: 'pipelines#charts') do
+            = link_to charts_namespace_project_pipelines_path(@project.namespace, @project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
               %span
-                Cycle Analytics
+                Charts
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
index a0b14a7274a24a198ca92f1cee60660dc5d12911..3feb99cfcd7323fdb31afb8ab139ff42a208045f 100644
--- a/app/views/projects/pipelines/_stage.html.haml
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -1,3 +1,5 @@
-- @stage.statuses.latest.each do |status|
-  %li
-    = render 'ci/status/dropdown_graph_badge', subject: status
+- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status)
+- HasStatus::ORDERED_STATUSES.each do |ordered_status|
+  - grouped_statuses.fetch(ordered_status, []).each do |status|
+    %li
+      = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4a5043aac3c38b2030abce3e018df9deb8f2bd8a
--- /dev/null
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -0,0 +1,21 @@
+- @no_container = true
+- page_title "Charts", "Pipelines"
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_d3')
+  = page_specific_javascript_bundle_tag('graphs')
+= render 'head'
+
+%div{ class: container_class }
+  .sub-header-block
+    .oneline
+      A collection of graphs for Continuous Integration
+
+  #charts.ci-charts
+    .row
+      .col-md-6
+        = render 'projects/pipelines/charts/overall'
+      .col-md-6
+        = render 'projects/pipelines/charts/build_times'
+
+    %hr
+    = render 'projects/pipelines/charts/builds'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/pipelines/charts/_build_times.haml
similarity index 100%
rename from app/views/projects/graphs/ci/_build_times.haml
rename to app/views/projects/pipelines/charts/_build_times.haml
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/pipelines/charts/_builds.haml
similarity index 100%
rename from app/views/projects/graphs/ci/_builds.haml
rename to app/views/projects/pipelines/charts/_builds.haml
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml
similarity index 100%
rename from app/views/projects/graphs/ci/_overall.haml
rename to app/views/projects/pipelines/charts/_overall.haml
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 6e0428e2a31cc9b3b83ff75d224d108cb9ac912d..5d59ce06612d9c639bfcdc1a7191ac403ac32473 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,35 @@
 %div{ class: container_class }
   .top-area
     %ul.nav-links
-      %li{ class: active_when(@scope.nil?) }>
+      %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
         = link_to project_pipelines_path(@project) do
           All
           %span.badge.js-totalbuilds-count
             = number_with_delimiter(@pipelines_count)
 
-      %li{ class: active_when(@scope == 'running') }>
+      %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
+        = link_to project_pipelines_path(@project, scope: :pending) do
+          Pending
+          %span.badge
+            = number_with_delimiter(@pending_count)
+
+      %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
         = link_to project_pipelines_path(@project, scope: :running) do
           Running
           %span.badge.js-running-count
-            = number_with_delimiter(@running_or_pending_count)
+            = number_with_delimiter(@running_count)
+
+      %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
+        = link_to project_pipelines_path(@project, scope: :finished) do
+          Finished
+          %span.badge
+            = number_with_delimiter(@finished_count)
 
-      %li{ class: active_when(@scope == 'branches') }>
+      %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
         = link_to project_pipelines_path(@project, scope: :branches) do
           Branches
 
-      %li{ class: active_when(@scope == 'tags') }>
+      %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
         = link_to project_pipelines_path(@project, scope: :tags) do
           Tags
 
@@ -36,28 +48,7 @@
         = link_to ci_lint_path, class: 'btn btn-default' do
           %span CI Lint
   .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
-    .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
-      "icon_status_canceled" => custom_icon("icon_status_canceled"),
-      "icon_status_running" => custom_icon("icon_status_running"),
-      "icon_status_skipped" => custom_icon("icon_status_skipped"),
-      "icon_status_created" => custom_icon("icon_status_created"),
-      "icon_status_pending" => custom_icon("icon_status_pending"),
-      "icon_status_success" => custom_icon("icon_status_success"),
-      "icon_status_failed" => custom_icon("icon_status_failed"),
-      "icon_status_warning" => custom_icon("icon_status_warning"),
-      "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
-      "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
-      "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
-      "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
-      "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
-      "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
-      "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
-      "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
-      "icon_play" => custom_icon("icon_play"),
-      "icon_timer" => custom_icon("icon_timer"),
-      "icon_status_manual" => custom_icon("icon_status_manual"),
-    } }
-
-      .vue-pipelines-index
+    .vue-pipelines-index
 
+= page_specific_javascript_bundle_tag('common_vue')
 = page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 55202725b9ee74c808a4b96148fd5365b93a1d8b..14a270a3039dc175695373cda86924a16132ceb3 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,11 @@
   .form-group
     = f.label :ref, 'Create for', class: 'control-label'
     .col-sm-10
-      = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
+      = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
+      = dropdown_tag(params[:ref] || @project.default_branch,
+                     options: { toggle_class: 'js-branch-select wide',
+                                filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+                                data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
       .help-block Existing branch name, tag
   .form-actions
     = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 04b19a8c5a7389c97f7ad9575275a0ce96cc80b2..cf0db9438657c843f30c9e39b9415004705ea8d6 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -23,6 +23,6 @@
           - if can_admin_project
             %th
       %tbody
-        = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
+        = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
 
     = paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index e95a3b1b4c368810e70e0197d3620826df36e0c9..b8e885b4d9a7a938b9b7dec2151d262996221b52 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -10,7 +10,7 @@
           = f.label :name, class: 'col-md-2 text-right' do
             Branch:
           .col-md-10
-            = render partial: "dropdown", locals: { f: f }
+            = render partial: "projects/protected_branches/dropdown", locals: { f: f }
             .help-block
               = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
               such as
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/_index.html.haml
similarity index 86%
rename from app/views/projects/protected_branches/index.html.haml
rename to app/views/projects/protected_branches/_index.html.haml
index b3b419bd92d1eb1c854b25d7ce495f764e124313..2d8c519c025dddfab47b474b017ceacd6c6cc821 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/_index.html.haml
@@ -1,11 +1,10 @@
-- page_title "Protected branches"
 - content_for :page_specific_javascripts do
   = page_specific_javascript_bundle_tag('protected_branches')
 
 .row.prepend-top-default.append-bottom-default
   .col-lg-3
     %h4.prepend-top-0
-      = page_title
+      Protected Branches
     %p Keep stable branches secure and force developers to use merge requests.
     %p.prepend-top-20
       By default, protected branches are designed to:
@@ -17,6 +16,6 @@
       %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
   .col-lg-9
     - if can? current_user, :admin_project, @project
-      = render 'create_protected_branch'
+      = render 'projects/protected_branches/create_protected_branch'
 
-    = render "branches_list"
+    = render "projects/protected_branches/branches_list"
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0193800dedfd4015312c767f8e9a6e152d815c7f..b2a6b8469a35d6f8cb45199b1b541c40dbfe44d3 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -14,7 +14,7 @@
       - else
         (branch was removed from repository)
 
-  = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+  = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch }
 
   - if can_admin_project
     %td
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 3a323d94cc2e70daa3ae90b168d577cdd9f4c145..2fb88297fb322874995ba4b2d1e1cbbb519bb34b 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -4,13 +4,13 @@
 %ul.list-unstyled.indent-list
   %li
     1.
-    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
       Enable custom slash commands
       = icon('external-link')
     on your Mattermost installation
   %li
     2.
-    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
       Add a slash command
       = icon('external-link')
     in your Mattermost team with these options:
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 a04fd5035a6fb3660ef423f2b16a0e6b46dff554..2a1b9d4c465def74bdf99a0c2898beab45c09ceb 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
   %p
     This service allows users to perform common operations on this
     project by entering slash commands in Mattermost.
-    = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+    = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
       View documentation
       = icon('external-link')
   %p.inline
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 0d973a20d4c9ccc18feebf5a3bb72db56e49a6ce..078b7be68650925878111a9e6f414b9ba870f05e 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
   %p
     This service allows users to perform common operations on this
     project by entering slash commands in Slack.
-    = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+    = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
       View documentation
       = icon('external-link')
   %p.inline
@@ -57,7 +57,7 @@
         = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
         .col-sm-10.col-xs-12.text-block
           = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
-          = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+          = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
 
       .form-group
         = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..88bcb541dac279fcd90cac63eea92ae70f3f8871
--- /dev/null
+++ b/app/views/projects/settings/_head.html.haml
@@ -0,0 +1,33 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: container_class }
+        - can_edit = can?(current_user, :admin_project, @project)
+        - if can_edit
+          = nav_link(controller: :projects) do
+            = link_to edit_project_path(@project), title: 'General' do
+              %span
+                General
+        = nav_link(controller: :members) do
+          = link_to project_settings_members_path(@project), title: 'Members' do
+            %span
+              Members
+        - if can_edit
+          = nav_link(controller: :integrations) do
+            = link_to project_settings_integrations_path(@project), title: 'Integrations' do
+              %span
+                Integrations
+          = nav_link(controller: :repository) do
+            = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
+              %span
+                Repository
+          - if @project.feature_available?(:builds, current_user)
+            = nav_link(controller: :ci_cd) do
+              = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+                %span
+                  CI/CD Pipelines
+          = nav_link(controller: :pages) do
+            = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
+              %span
+                Pages
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 52f5f7b81e2afb493fe05a4e1ea792fd521f27eb..e26030960145c37209c1af0b1b5e01336df01c76 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,4 +1,5 @@
 - page_title "CI/CD Pipelines"
+= render "projects/settings/head"
 
 = render 'projects/runners/index'
 = render 'projects/variables/index'
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index aa38a889cdd70cf9c6535cc910b818f78d208317..f69992566b5edaf472ee853a69263086d7a32663 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,3 +1,4 @@
 - page_title 'Integrations'
+= render "projects/settings/head"
 = render 'projects/hooks/index'
 = render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index d81ed7bb6097af815cb5fc6d40e47037e5082365..20e1ad682443acd887eca582bd3c8c454ee34d3f 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,4 +1,5 @@
 - page_title "Members"
+= render "projects/settings/head"
 
 = render "projects/project_members/index"
 - if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4c02302e1618e0d601b5cda9394032609aa10735
--- /dev/null
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -0,0 +1,5 @@
+- page_title "Repository"
+= render "projects/settings/head"
+
+= render @deploy_keys
+= render "projects/protected_branches/index"
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 11310d5e1e1f51cdc956bf965558eec40921460c..5c7f2e315f040ef4f0fe614ad674a707695a91ce 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -1,7 +1,7 @@
 xml.instruct!
 xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
   xml.title   "#{@project.name} activity"
-  xml.link    href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  xml.link    href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml"
   xml.link    href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
   xml.id      namespace_project_url(@project.namespace, @project)
   xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f7419728719e073b1e3796990168f90365ea4204..de1229d58aaefcdcf8bc84ec6d20716a9b054e5f 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,80 +1,80 @@
 - @no_container = true
 
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
+  = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, rss_url_options), title: "#{@project.name} activity")
 
 = content_for :flash_message do
   - if current_user && can?(current_user, :download_code, @project)
     = render 'shared/no_ssh'
     = render 'shared/no_password'
 
-= render 'projects/last_push'
+= render "projects/head"
+= render "projects/last_push"
 = render "home_panel"
 
 - if current_user && can?(current_user, :download_code, @project)
-  .project-stats-container{ class: container_class }
-    %nav.project-stats
-      %ul.nav
-        %li
-          = link_to project_files_path(@project) do
-            Files (#{storage_counter(@project.statistics.total_repository_size)})
-        %li
-          = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
-            #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
-        %li
-          = link_to namespace_project_branches_path(@project.namespace, @project) do
-            #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
-        %li
-          = link_to namespace_project_tags_path(@project.namespace, @project) do
-            #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
+  %nav.project-stats{ class: container_class }
+    %ul.nav
+      %li
+        = link_to project_files_path(@project) do
+          Files (#{storage_counter(@project.statistics.total_repository_size)})
+      %li
+        = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+          #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
+      %li
+        = link_to namespace_project_branches_path(@project.namespace, @project) do
+          #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
+      %li
+        = link_to namespace_project_tags_path(@project.namespace, @project) do
+          #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
 
-        - if default_project_view != 'readme' && @repository.readme
-          %li
-            = link_to 'Readme', readme_path(@project)
+      - if default_project_view != 'readme' && @repository.readme
+        %li
+          = link_to 'Readme', readme_path(@project)
 
-        - if @repository.changelog
-          %li
-            = link_to 'Changelog', changelog_path(@project)
+      - if @repository.changelog
+        %li
+          = link_to 'Changelog', changelog_path(@project)
 
-        - if @repository.license_blob
-          %li
-            = link_to license_short_name(@project), license_path(@project)
+      - if @repository.license_blob
+        %li
+          = link_to license_short_name(@project), license_path(@project)
 
-        - if @repository.contribution_guide
-          %li
-            = link_to 'Contribution guide', contribution_guide_path(@project)
+      - if @repository.contribution_guide
+        %li
+          = link_to 'Contribution guide', contribution_guide_path(@project)
 
-        - if @repository.gitlab_ci_yml
-          %li
-            = link_to 'CI configuration', ci_configuration_path(@project)
+      - if @repository.gitlab_ci_yml
+        %li
+          = link_to 'CI configuration', ci_configuration_path(@project)
 
-        - if current_user && can_push_branch?(@project, @project.default_branch)
-          - unless @repository.changelog
-            %li.missing
-              = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
-                Add Changelog
-          - unless @repository.license_blob
-            %li.missing
-              = link_to add_special_file_path(@project, file_name: 'LICENSE') do
-                Add License
-          - unless @repository.contribution_guide
-            %li.missing
-              = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
-                Add Contribution guide
-          - unless @repository.gitlab_ci_yml
-            %li.missing
-              = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
-                Set up CI
-          - if koding_enabled? && @repository.koding_yml.blank?
-            %li.missing
-              = link_to 'Set up Koding', add_koding_stack_path(@project)
-          - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
-            %li.missing
-              = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
-                Set up auto deploy
+      - if current_user && can_push_branch?(@project, @project.default_branch)
+        - unless @repository.changelog
+          %li.missing
+            = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
+              Add Changelog
+        - unless @repository.license_blob
+          %li.missing
+            = link_to add_special_file_path(@project, file_name: 'LICENSE') do
+              Add License
+        - unless @repository.contribution_guide
+          %li.missing
+            = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
+              Add Contribution guide
+        - unless @repository.gitlab_ci_yml
+          %li.missing
+            = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+              Set up CI
+        - if koding_enabled? && @repository.koding_yml.blank?
+          %li.missing
+            = link_to 'Set up Koding', add_koding_stack_path(@project)
+        - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+          %li.missing
+            = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+              Set up auto deploy
 
-    - if @repository.commit
+  - if @repository.commit
+    %div{ class: container_class }
       .project-last-commit
         = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
 
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 6b3d7d4008b96335327d05b5b6625bea88c553d2..e35385f4cabd963793460cb3d781c49506bdc12c 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,13 +4,7 @@
 
 .project-snippets
   %article.file-holder.snippet-file-content
-    .js-file-title.file-title
-      = blob_icon 0, @snippet.file_name
-      = @snippet.file_name
-      .file-actions
-        = 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 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
 
   .row-content-block.top-block.content-component-block
     = render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 8ef069b9e057f2a2f4d9397087cc372388153769..dffe908e85a514335d66f7590fbeaca4555524ba 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -23,7 +23,7 @@
             = markdown_field(release, :description)
 
   .row-fixed-content.controls
-    = render 'projects/buttons/download', project: @project, ref: tag.name
+    = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
 
     - if can?(current_user, :push_code, @project)
       = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
index e4a78fadbebb6b005b85337652d6cdfd0631dd4a..cde23e03d5418b17bad3993156e1ef7e9914c858 100644
--- a/app/views/projects/tags/destroy.js.haml
+++ b/app/views/projects/tags/destroy.js.haml
@@ -1,2 +1,4 @@
-- if @repository.tags.empty?
+- if @error.present?
+  new Flash('#{escape_javascript(@error)}', 'alert');
+- elsif @repository.tags.empty?
   $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index e2f132f77428c7551cf9cba226347b9915a3818c..7f9a44e565f61387838d5d55893d1a6ed49d9949 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -3,7 +3,7 @@
 = render "projects/commits/head"
 
 .flex-list{ class: container_class }
-  .top-area.flex-row
+  .top-area.adjust
     .nav-text.row-main-content
       Tags give the ability to mark specific points in history as being important
 
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 9864be3562a8b1e8ab686a79bd9e109cb0322ded..a2a260392208937a25a8fa62d1a2fc26af4a4941 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -2,8 +2,7 @@
 
 - page_title @path.presence || "Files", @ref
 = content_for :meta_tags do
-  - if current_user
-    = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+  = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
 = render "projects/commits/head"
 = render 'projects/last_push'
 
diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ea32eac2ae2f28a3bab7cb04bba839837835e6fb
--- /dev/null
+++ b/app/views/projects/triggers/_content.html.haml
@@ -0,0 +1,14 @@
+%h4.prepend-top-0
+  Triggers
+%p.prepend-top-20
+  Triggers can force a specific branch or tag to get rebuilt with an API call.  These tokens will
+  impersonate their associated user including their access to projects and their project
+  permissions.
+%p.prepend-top-20
+  Triggers with the
+  %span.label.label-primary legacy
+  label do not have an associated user and only have access to the current project.
+%p.append-bottom-0
+  = succeed '.' do
+    Learn more in the
+    = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5f708b3a2eddbe1e74a98d177c6ec03a25c39579
--- /dev/null
+++ b/app/views/projects/triggers/_form.html.haml
@@ -0,0 +1,11 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f|
+  = form_errors(@trigger)
+
+  - if @trigger.token
+    .form-group
+      %label.label-light Token
+      %p.form-control-static= @trigger.token
+  .form-group
+    = f.label :key, "Description", class: "label-light"
+    = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
+  = f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index 33883facf9b879a306f6059159f84475b4bc7459..cc74e50a5e37ae97a3685c0c2ce60de7f6d4602d 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,35 +1,31 @@
-.row.prepend-top-default.append-bottom-default
+.row.prepend-top-default.append-bottom-default.triggers-container
   .col-lg-3
-    %h4.prepend-top-0
-      Triggers
-    %p.prepend-top-20
-      Triggers can force a specific branch or tag to get rebuilt with an API call.
-    %p.append-bottom-0
-      = succeed '.' do
-        Learn more in the
-        = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
+    = render "projects/triggers/content"
   .col-lg-9
     .panel.panel-default
       .panel-heading
         %h4.panel-title
           Manage your project's triggers
       .panel-body
+        = render "projects/triggers/form", btn_text: "Add trigger"
+        %hr
         - if @triggers.any?
-          .table-responsive
+          .table-responsive.triggers-list
             %table.table
               %thead
                 %th
                   %strong Token
+                %th
+                  %strong Description
+                %th
+                  %strong Owner
                 %th
                   %strong Last used
                 %th
               = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
         - else
           %p.settings-message.text-center.append-bottom-default
-            No triggers have been created yet. Add one using the button below.
-
-        = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
-          = f.submit "Add trigger", class: 'btn btn-success'
+            No triggers have been created yet. Add one using the form above.
 
       .panel-footer
 
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 112b51712ef90728b13b1f6aacc49d1e77f8235b..ed68e0ed56db6ef71c5193e34631cb9a4d801613 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -1,12 +1,42 @@
 %tr
   %td
-    %span.monospace= trigger.token
+    - if can?(current_user, :admin_trigger, trigger)
+      %span= trigger.token
+      = clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard")
+    - else
+      %span= trigger.short_token
+
+    .label-container
+      - if trigger.legacy?
+        %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
+      - if !trigger.can_access_project?
+        %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
+
+  %td
+    - if trigger.description? && trigger.description.length > 15
+      %span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15)
+    - else
+      = trigger.description
+
+  %td
+    - if trigger.owner
+      .trigger-owner.sr-only= trigger.owner.name
+      = user_avatar(user: trigger.owner, size: 20)
 
   %td
-    - if trigger.last_trigger_request
-      #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
+    - if trigger.last_used
+      #{time_ago_in_words(trigger.last_used)} ago
     - else
       Never
 
-  %td.text-right
-    = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm"
+  %td.text-right.trigger-actions
+    - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?"
+    - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
+    - if trigger.owner != current_user && can?(current_user, :manage_trigger, trigger)
+      = link_to 'Take ownership', take_ownership_namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: take_ownership_confirmation }, method: :post, class: "btn btn-default btn-sm btn-trigger-take-ownership"
+    - if can?(current_user, :admin_trigger, trigger)
+      = link_to edit_namespace_project_trigger_path(@project.namespace, @project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do
+        %i.fa.fa-pencil
+    - if can?(current_user, :manage_trigger, trigger)
+      = link_to namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
+        %i.fa.fa-trash
diff --git a/app/views/projects/triggers/edit.html.haml b/app/views/projects/triggers/edit.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c35df322b9dfd628a68d7a8614a27e948b845210
--- /dev/null
+++ b/app/views/projects/triggers/edit.html.haml
@@ -0,0 +1,9 @@
+- page_title "Trigger"
+
+.row.prepend-top-default.append-bottom-default
+  .col-lg-3
+    = render "content"
+  .col-lg-9
+    %h4.prepend-top-0
+      Update trigger
+    = render "form", btn_text: "Save trigger"
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 763c2fea39b4eea90d5f55e65783c395a073c3db..5211ade1a5f6c280a32edc234b85189d8f07f8d7 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -4,6 +4,6 @@
       New Page
   = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
     Page History
-  - if can?(current_user, :create_wiki, @project)
+  - if can?(current_user, :create_wiki, @project) && @page.latest?
     = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
       Edit
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 22004ecacbc85938b64db96e19155ac5421e2bb8..02133d09cdf6fa543ccf8593291c41362964fcdc 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -11,7 +11,7 @@
 
   .results.prepend-top-10
     - if @scope == 'commits'
-      %ul.content-list.commit-list.table-list.table-wide
+      %ul.content-list.commit-list
         = render partial: "search/results/commit", collection: @search_objects
     - else
       .search-results
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7799aff6b5b347e664b92144d9b2dde5f2c6f800
--- /dev/null
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -0,0 +1,8 @@
+- dropdown_toggle_text = @target_branch || tree_edit_branch
+= hidden_field_tag 'target_branch', dropdown_toggle_text
+
+.dropdown
+  = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+  .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
+    = render partial: 'shared/projects/blob/branch_page_default'
+    = render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index efb207b9916c2331b12abd3b78aa75d8c69a19f9..c2d9ac87b20419292422a3e677309504d9d7c58d 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -17,8 +17,9 @@
           %strong= parent.full_path + '/'
       = f.text_field :path, placeholder: 'open-source', class: 'form-control',
         autofocus: local_assigns[:autofocus] || false, required: true,
-        pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
-        title: 'Please choose a group name with no special characters.'
+        pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
+        title: 'Please choose a group name with no special characters.',
+        "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
       - if parent
         = f.hidden_field :parent_id, value: parent.id
 
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 1264e524d860ab2601c8e73ed37dd9dac1afd260..1d4fd71522d4628d96ab2f65ba4cba0c5f61fb64 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -2,6 +2,12 @@
 - issue_votes        = @issuable_meta_data[issuable.id]
 - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
 - issuable_url       = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
+- issuable_mr        = @issuable_meta_data[issuable.id].merge_requests_count
+
+- if issuable_mr > 0
+  %li
+    = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
+    = issuable_mr
 
 - if upvotes > 0
   %li
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 1744a597c5196b2473c806f5b3e397c79a040967..bd994cdad01044a1b1b54e9115a4061de4c3bb9f 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -45,11 +45,11 @@
     - if current_user && defined?(@project)
       .label-subscription.inline
         - if label.is_a?(ProjectLabel)
-          %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+          %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
             %span= label_subscription_toggle_button_text(label, @project)
             = icon('spinner spin', class: 'label-subscribe-button-loading')
         - else
-          %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } }
+          %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
             %span Unsubscribe
             = icon('spinner spin', class: 'label-subscribe-button-loading')
 
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 9b67422da2c2532586fc689dfabee1acaef34fc1..10e6c49ae9fbdd7e6bc25206da25aa269e502c4f 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg width="36" height="36" class="tanuki-logo">
+<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36">
   <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
   <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
   <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 704893b4d5b29126b4b14f02e0d2752c00df6d33..57a0eaa919e9192e85e06047ffb760f0d4bc045d 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,19 +1,13 @@
-- if @project
-  - counts = milestone_counts(@project.milestones)
-
 %ul.nav-links
   %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]
+      %span.badge= counts[:opened]
   %li{ class: milestone_class_for_state(params[:state], 'closed') }>
     = link_to milestones_filter_path(state: 'closed') do
       Closed
-      - if @project
-        %span.badge= counts[:closed]
+      %span.badge= counts[:closed]
   %li{ class: milestone_class_for_state(params[:state], 'all') }>
     = link_to milestones_filter_path(state: 'all') do
       All
-      - if @project
-        %span.badge= counts[:all]
+      %span.badge= counts[:all]
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0c8ac48bb58f8d3ced26fbbf7d54f7bb58f61d83..3ac5e15d1c4d6353264de7e2ad16a4ce089742b5 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
     .form-group.branch
       = label_tag 'target_branch', 'Target branch', class: 'control-label'
       .col-sm-10
-        = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
+        = render 'shared/branch_switcher'
 
         .js-create-merge-request-container
           .checkbox
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..af4cc90f4a7230dd095db26a4524ef8674162883
--- /dev/null
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -0,0 +1,39 @@
+- type = impersonation ? "Impersonation" : "Personal Access"
+
+%h5.prepend-top-0
+  Add a #{type} Token
+%p.profile-settings-content
+  Pick a name for the application, and we'll give you a unique #{type} Token.
+
+= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
+
+  = form_errors(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: token, scopes: scopes
+
+  .prepend-top-default
+    = f.submit "Create #{type} Token", class: "btn btn-create"
+
+:javascript
+  var $dateField = $('.datepicker');
+  var date = $dateField.val();
+
+  new Pikaday({
+    field: $dateField.get(0),
+    theme: 'gitlab-theme',
+    format: 'yyyy-mm-dd',
+    minDate: new Date(),
+    onSelect: function(dateText) {
+      $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+    }
+  });
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..67a49815478bb1e0b19c4c7a28ed305bb1aa246b
--- /dev/null
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -0,0 +1,60 @@
+- type = impersonation ? "Impersonation" : "Personal Access"
+%hr
+
+%h5 Active #{type} Tokens (#{active_tokens.length})
+- if impersonation
+  %p.profile-settings-content
+    To see all the user's personal access tokens you must impersonate them first.
+
+- if active_tokens.present?
+  .table-responsive
+    %table.table.active-tokens
+      %thead
+        %tr
+          %th Name
+          %th Created
+          %th Expires
+          %th Scopes
+          - if impersonation
+            %th Token
+          %th
+      %tbody
+        - active_tokens.each do |token|
+          %tr
+            %td= token.name
+            %td= token.created_at.to_date.to_s(:medium)
+            %td
+              - if token.expires?
+                %span{ class: ('text-warning' if token.expires_soon?) }
+                  In #{distance_of_time_in_words_to_now(token.expires_at)}
+              - else
+                %span.token-never-expires-label Never
+            %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
+            - if impersonation
+              %td.token-token-container
+                = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control"
+                = clipboard_button(clipboard_text: token.token)
+            - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
+            %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+- else
+  .settings-message.text-center
+    This user has no active #{type} Tokens.
+
+%hr
+
+%h5 Inactive #{type} Tokens (#{inactive_tokens.length})
+- if inactive_tokens.present?
+  .table-responsive
+    %table.table.inactive-tokens
+      %thead
+        %tr
+          %th Name
+          %th Created
+      %tbody
+        - inactive_tokens.each do |token|
+          %tr
+            %td= token.name
+            %td= token.created_at.to_date.to_s(:medium)
+- else
+  .settings-message.text-center
+    This user has no inactive #{type} Tokens.
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 0ce0d759e86764ea27f79286418646175914636b..a212c714826d4bc2160e85e2ea0337a9bc2bbeeb 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
 .dropdown.inline.prepend-left-10
   %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
-    %span.light
     - if @sort.present?
       = sort_options_hash[@sort]
     - else
@@ -10,6 +9,8 @@
     %li
       = link_to page_filter_path(sort: sort_value_priority, label: true) do
         = sort_title_priority
+      = link_to page_filter_path(sort: sort_value_label_priority, label: true) do
+        = sort_title_label_priority
       = link_to page_filter_path(sort: sort_value_recently_created, label: true) do
         = sort_title_recently_created
       = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e20336540182f0915d67e8d1fd5e8f5200219c8f..7a7e3d467969b115fbd39a27b8ba37b35b117446 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
           Also, issues are searchable and filterable.
         - if project_select_button
           = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
-        - else
-          = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
       - else
-        %h4.text-center There are no issues to show.
+        %h4 There are no issues to show.
+      = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index ba5c2dae09dba48620f767124e52abcbb2288593..00fb77bdb3b8e35db42e98101e3b6fea104502e6 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -5,7 +5,7 @@
   .col-xs-12.col-sm-6
     .text-content
       %h4 Labels can be applied to issues and merge requests to categorize them.
-      %p You can also star label to make it a priority label.
+      %p You can also star a label to make it a priority label.
       - if can?(current_user, :admin_label, @project)
         = link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
         = link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..37589b634fa268708fd863eeb2af97e6bf11949e
--- /dev/null
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -0,0 +1,18 @@
+.dropdown.inline
+  %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+    %span.light
+    - if @sort.present?
+      = sort_options_hash[@sort]
+    - else
+      = sort_title_recently_created
+    = icon('chevron-down')
+  %ul.dropdown-menu.dropdown-menu-align-right
+    %li
+      = link_to filter_groups_path(sort: sort_value_recently_created) do
+        = sort_title_recently_created
+      = link_to filter_groups_path(sort: sort_value_oldest_created) do
+        = sort_title_oldest_created
+      = link_to filter_groups_path(sort: sort_value_recently_updated) do
+        = sort_title_recently_updated
+      = link_to filter_groups_path(sort: sort_value_oldest_updated) do
+        = sort_title_oldest_updated
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 60ca23ef680b9cc4170e85f28c0fa45667dbdfd5..a95020a9be853b9fa0e0d2130f0651bea162e573 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,5 +1,6 @@
 - group_member = local_assigns[:group_member]
 - full_name = true unless local_assigns[:full_name] == false
+- group_name = full_name ? group.full_name : group.name
 - css_class = '' unless local_assigns[:css_class]
 - css_class += " no-description" if group.description.blank?
 
@@ -28,11 +29,7 @@
   .avatar-container.s40
     = image_tag group_icon(group), class: "avatar s40 hidden-xs"
   .title
-    = link_to group, class: 'group-name' do
-      - if full_name
-        = group.full_name
-      - else
-        = group.name
+    = link_to group_name, group, class: 'group-name'
 
     - if group_member
       as
diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ad7a7faedf1d0443a69d03ae3fbcf6916f3692ea
--- /dev/null
+++ b/app/views/shared/groups/_search_form.html.haml
@@ -0,0 +1,2 @@
+= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f|
+  = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
diff --git a/app/views/shared/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb
new file mode 100644
index 0000000000000000000000000000000000000000..917753fb343036b17ad9d5579822040c5eddd4bb
--- /dev/null
+++ b/app/views/shared/icons/_collapse.svg.erb
@@ -0,0 +1 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/views/shared/icons/_icon_customization.svg b/app/views/shared/icons/_icon_customization.svg
new file mode 100644
index 0000000000000000000000000000000000000000..eb1f8ba129b0da61189df1a21509bf0177f4a307
--- /dev/null
+++ b/app/views/shared/icons/_icon_customization.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d1c541523abc2b3dd102ecff9664a7945cfbb1e8
--- /dev/null
+++ b/app/views/shared/icons/_icon_mattermost.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M250.05 34c1.9.04 3.8.11 5.6.2l-29.79 35.51c-.07.01-.15.03-.23.04C149.26 84.1 98.22 146.5 98.22 222.97c0 41.56 23.07 90.5 59.75 119.1 28.61 22.32 64.29 36.9 101.21 36.9 93.4 0 160.15-68.61 160.15-156 0-34.91-15.99-72.77-41.76-100.76l-1.63-47.39c54.45 39.15 89.95 103.02 90.06 175.17v.01c0 119.29-96.7 216-216 216-119.29 0-216-96.71-216-216S130.71 34 250 34h.05zm64.1 20.29c.66-.04 1.32.03 1.96.25 3.01 1 3.85 3.57 3.93 6.45l3.84 146.88c.76 28.66-17.16 68.44-60.39 68.56-30.97.08-63.68-20.83-63.68-60.13.01-14.73 5.61-31.26 19.25-48.11l90.03-111.18c1.15-1.42 3.08-2.58 5.06-2.72z"/></svg>
diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ae219a3ded2ed26ff8e01b86b036ef10b6a00929
--- /dev/null
+++ b/app/views/shared/icons/_icon_mr_issue.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
\ No newline at end of file
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index f17ae9f28eb6e5e4c42068c7507158ac5d4e316f..847a86e2e68a4acde327efcb7354c8f25186b036 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,4 @@
-- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
+- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
 - boards_page = controller.controller_name == 'boards'
 
 .issues-filters
@@ -24,7 +24,7 @@
             placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
 
         .filter-item.inline.milestone-filter
-          = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true
+          = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
 
         .filter-item.inline.labels-filter
           = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
@@ -34,21 +34,7 @@
             %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
 
         .pull-right
-          - if boards_page
-            #js-boards-search.issue-boards-search
-              %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
-              - if can?(current_user, :admin_list, @project)
-                #js-add-issues-btn.pull-right.prepend-left-10
-                .dropdown.pull-right
-                  %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
-                    Add list
-                  .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
-                    = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
-                    - if can?(current_user, :admin_label, @project)
-                      = render partial: "shared/issuable/label_page_create"
-                    = dropdown_loading
-          - else
-            = render 'shared/sort_dropdown'
+          = render 'shared/sort_dropdown'
 
     - if @bulk_edit
       .issues_bulk_update.hide
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index cb92b2e97a73845428628a9a7c357707e786c8d8..17107f55a2d618f9746574b99ce0fed3214ea64e 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
   .alert.alert-danger
     Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
     Please check out
-    = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+    = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
     and make sure your changes will not unintentionally remove theirs
 
 .form-group
@@ -45,41 +45,47 @@
 
 = render 'shared/issuable/form/merge_params', issuable: issuable
 
-- if @merge_request_for_resolving_discussions
+- if @merge_request_to_resolve_discussions_of
   .form-group
     .col-sm-10.col-sm-offset-2
-      - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user)
-        = icon('exclamation-triangle')
-        Creating this issue will mark all discussions in
-        = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
-        as resolved.
-        = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid
+      = icon('info-circle')
+      - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
+        = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
+        - if @discussion_to_resolve
+          = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
+          Creating this issue will resolve the discussion in
+        - else
+          Creating this issue will resolve all discussions in
+        = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
       - else
-        = icon('exclamation-triangle')
-        You can't automatically mark all discussions in
-        = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
-        as resolved. Ask someone with sufficient rights to resolve the them.
+        The
+        = @discussion_to_resolve ? 'discussion' : 'discussions'
+        at
+        = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+        will stay unresolved. Ask someone with permission to resolve
+        = @discussion_to_resolve ? 'it.' : 'them.'
 
 - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
 .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
-  - if issuable.new_record?
-    = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
-  - else
-    = form.submit 'Save changes', class: 'btn btn-save'
+  .pull-right
+    - if issuable.new_record?
+      = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
+    - else
+      - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
+        = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
+      = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
+
+  %span.append-right-10
+    - if issuable.new_record?
+      = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+    - else
+      = form.submit 'Save changes', class: 'btn btn-save'
 
   - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
-    .inline.prepend-left-10
+    .inline.prepend-top-10
       Please review the
       %strong= link_to('contribution guidelines', guide_url)
       for this project.
 
-  - if issuable.new_record?
-    = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
-  - else
-    .pull-right
-      - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
-        = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
-                                                                                                  method: :delete, class: 'btn btn-danger btn-grouped'
-      = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
 
 = form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 415361f8fbf0ba453cf1363a5f03763a5a3d382c..f0d50828e2a40cbf8058b37a1c4882b67aea5af4 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -6,7 +6,7 @@
 - if selected.present? || params[:milestone_title].present?
   = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
 = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
-  placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+  placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
   - if project
     %ul.dropdown-footer-list
       - if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 8e04b50bb8ac1a535ab181ac9794775563f7798c..b58640c3ef0bc035da1d95e0bcefe1b71049a4b1 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,7 +1,8 @@
 - type = local_assigns.fetch(:type)
+- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
 
 .issues-filters
-  .issues-details-filters.row-content-block.second-block.filtered-search-block
+  .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
     = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
       - if params[:search].present?
         = hidden_field_tag :search, params[:search]
@@ -11,18 +12,21 @@
             class: "check_all_issues left"
       .issues-other-filters.filtered-search-container
         .filtered-search-input-container
-          %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
-          = icon('filter')
-          %button.clear-search.hidden{ type: 'button' }
-            = icon('times')
+          .scroll-container
+            %ul.tokens-container.list-unstyled
+              %li.input-token
+                %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
+            = icon('filter')
+            %button.clear-search.hidden{ type: 'button' }
+              = icon('times')
           #js-dropdown-hint.dropdown-menu.hint-dropdown
-            %ul{ 'data-dropdown' => true }
-              %li.filter-dropdown-item{ 'data-action' => 'submit' }
+            %ul{ data: { dropdown: true } }
+              %li.filter-dropdown-item{ data: { action: 'submit' } }
                 %button.btn.btn-link
                   = icon('search')
                   %span
                     Keep typing and press Enter
-            %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+            %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
               %li.filter-dropdown-item
                 %button.btn.btn-link
                   -# Encapsulate static class name `{{icon}}` inside #{} to bypass
@@ -33,57 +37,72 @@
                   %span.js-filter-tag.dropdown-light-content
                     {{tag}}
           #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
-            %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+            %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
               %li.filter-dropdown-item
                 %button.btn.btn-link.dropdown-user
-                  %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+                  %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
                   .dropdown-user-details
                     %span
                       {{name}}
                     %span.dropdown-light-content
                       @{{username}}
           #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
-            %ul{ 'data-dropdown' => true }
-              %li.filter-dropdown-item{ 'data-value' => 'none' }
+            %ul{ data: { dropdown: true } }
+              %li.filter-dropdown-item{ data: { value: 'none' } }
                 %button.btn.btn-link
                   No Assignee
               %li.divider
-            %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+            %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
               %li.filter-dropdown-item
                 %button.btn.btn-link.dropdown-user
-                  %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+                  %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
                   .dropdown-user-details
                     %span
                       {{name}}
                     %span.dropdown-light-content
                       @{{username}}
           #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
-            %ul{ 'data-dropdown' => true }
-              %li.filter-dropdown-item{ 'data-value' => 'none' }
+            %ul{ data: { dropdown: true } }
+              %li.filter-dropdown-item{ data: { value: 'none' } }
                 %button.btn.btn-link
                   No Milestone
-              %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+              %li.filter-dropdown-item{ data: { value: 'upcoming' } }
                 %button.btn.btn-link
                   Upcoming
+              %li.filter-dropdown-item{ 'data-value' => 'started' }
+                %button.btn.btn-link
+                  Started
               %li.divider
-            %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+            %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
               %li.filter-dropdown-item
                 %button.btn.btn-link.js-data-value
                   {{title}}
-          #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
-            %ul{ 'data-dropdown' => true }
-              %li.filter-dropdown-item{ 'data-value' => 'none' }
+          #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } }
+            %ul{ data: { dropdown: true } }
+              %li.filter-dropdown-item{ data: { value: 'none' } }
                 %button.btn.btn-link
                   No Label
               %li.divider
-            %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+            %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
               %li.filter-dropdown-item
                 %button.btn.btn-link
                   %span.dropdown-label-box{ style: 'background: {{color}}' }
                   %span.label-title.js-data-value
                     {{title}}
-        .pull-right
-          = render 'shared/sort_dropdown'
+        .filter-dropdown-container
+          - if type == :boards
+            - if can?(current_user, :admin_list, @project)
+              .dropdown.prepend-left-10#js-add-list
+                %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
+                  Add list
+                .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+                  = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+                  - if can?(current_user, :admin_label, @project)
+                    = render partial: "shared/issuable/label_page_create"
+                  = dropdown_loading
+              #js-add-issues-btn.prepend-left-10
+          - elsif type != :boards_modal
+            = render 'shared/sort_dropdown'
 
     - if @bulk_edit
       .issues_bulk_update.hide
@@ -112,22 +131,23 @@
 
           = hidden_field_tag 'update[issuable_ids]', []
           = hidden_field_tag :state_event, params[:state_event]
-          .filter-item.inline
+          .filter-item.inline.update-issues-btn
             = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
 
-:javascript
-  new UsersSelect();
-  new LabelsSelect();
-  new MilestoneSelect();
-  new IssueStatusSelect();
-  new SubscriptionSelect();
+- unless type === :boards_modal
+  :javascript
+    new UsersSelect();
+    new LabelsSelect();
+    new MilestoneSelect();
+    new IssueStatusSelect();
+    new SubscriptionSelect();
 
-  $(document).off('page:restore').on('page:restore', function (event) {
-    if (gl.FilteredSearchManager) {
-      new gl.FilteredSearchManager();
-    }
-    Issuable.init();
-    new gl.IssuableBulkActions({
-      prefixId: 'issue_',
+    $(document).off('page:restore').on('page:restore', function (event) {
+      if (gl.FilteredSearchManager) {
+        new gl.FilteredSearchManager();
+      }
+      Issuable.init();
+      new gl.IssuableBulkActions({
+        prefixId: 'issue_',
+      });
     });
-  });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3f7f1a86b9fdd5e53913ba2dfa0a0b6f332525d9..25a4aec0a384586a7704b7650c4b265df3bea357 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,6 @@
 - todo = issuable_todo(issuable)
 - content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_vue')
   = page_specific_javascript_bundle_tag('issuable')
 
 %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
@@ -9,16 +10,16 @@
       - if current_user
         %span.issuable-header-text.hide-collapsed.pull-left
           Todo
-      %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
+      %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
         = sidebar_gutter_toggle_icon
       - if current_user
-        %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+        %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
           %span.js-issuable-todo-text
             - if todo
               Mark done
             - else
               Add todo
-          = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
+          = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true')
 
     = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
       .block.assignee
@@ -26,10 +27,10 @@
           - if issuable.assignee
             = link_to_member(@project, issuable.assignee, size: 24)
           - else
-            = icon('user')
+            = icon('user', 'aria-hidden': 'true')
         .title.hide-collapsed
           Assignee
-          = icon('spinner spin', class: 'block-loading')
+          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
           - if can_edit_issuable
             = link_to 'Edit', '#', class: 'edit-link pull-right'
         .value.hide-collapsed
@@ -37,7 +38,7 @@
             = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
               - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
                 %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
-                  = icon('exclamation-triangle')
+                  = icon('exclamation-triangle', 'aria-hidden': 'true')
               %span.username
                 = issuable.assignee.to_reference
           - else
@@ -54,7 +55,7 @@
 
       .block.milestone
         .sidebar-collapsed-icon
-          = icon('clock-o')
+          = icon('clock-o', 'aria-hidden': 'true')
           %span
             - if issuable.milestone
               %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
@@ -63,7 +64,7 @@
               None
         .title.hide-collapsed
           Milestone
-          = icon('spinner spin', class: 'block-loading')
+          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
           - if can_edit_issuable
             = link_to 'Edit', '#', class: 'edit-link pull-right'
         .value.hide-collapsed
@@ -77,20 +78,20 @@
           = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
       - if issuable.has_attribute?(:time_estimate)
         #issuable-time-tracker.block
-          %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
+          %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'docs-url' => help_page_path('workflow/time_tracking.md') }
             // Fallback while content is loading
             .title.hide-collapsed
               Time tracking
-              = icon('spinner spin')
+              = icon('spinner spin', 'aria-hidden': 'true')
       - if issuable.has_attribute?(:due_date)
         .block.due_date
           .sidebar-collapsed-icon
-            = icon('calendar')
+            = icon('calendar', 'aria-hidden': 'true')
             %span.js-due-date-sidebar-value
               = issuable.due_date.try(:to_s, :medium) || 'None'
           .title.hide-collapsed
             Due date
-            = icon('spinner spin', class: 'block-loading')
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
               = link_to 'Edit', '#', class: 'edit-link pull-right'
           .value.hide-collapsed
@@ -110,7 +111,7 @@
               .dropdown
                 %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
                   %span.dropdown-toggle-text Due date
-                  = icon('chevron-down')
+                  = icon('chevron-down', 'aria-hidden': 'true')
                 .dropdown-menu.dropdown-menu-due-date
                   = dropdown_title('Due date')
                   = dropdown_content do
@@ -120,12 +121,12 @@
         - selected_labels = issuable.labels
         .block.labels
           .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
-            = icon('tags')
+            = icon('tags', class: 'hidden', 'aria-hidden': 'true')
             %span
               = selected_labels.size
           .title.hide-collapsed
             Labels
-            = icon('spinner spin', class: 'block-loading')
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can_edit_issuable
               = link_to 'Edit', '#', class: 'edit-link pull-right'
           .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
@@ -141,7 +142,7 @@
               %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
                 %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
                   = multi_label_name(selected_labels, "Labels")
-                = icon('chevron-down')
+                = icon('chevron-down', 'aria-hidden': 'true')
               .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
                 = render partial: "shared/issuable/label_page_default"
                 - if can? current_user, :admin_label, @project and @project
@@ -152,7 +153,7 @@
         - subscribed = issuable.subscribed?(current_user, @project)
         .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
           .sidebar-collapsed-icon
-            = icon('rss')
+            = icon('rss', 'aria-hidden': 'true')
           %span.issuable-header-text.hide-collapsed.pull-left
             Notifications
           - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
@@ -173,7 +174,7 @@
     :javascript
       gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
       new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
-      new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
+      new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
       new LabelsSelect();
       new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
       gl.Subscription.bindAll('.subscription');
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index a47085230b89bc13efde4e1f949f05214e27f5cd..9dbfedb84f1a2273b974eb2fd491df5037e14389 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -13,15 +13,15 @@
       = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
       .col-sm-10{ class: ("col-lg-8" if has_due_date) }
         .issuable-form-select-holder
-          - if issuable.assignee_id
-            = form.hidden_field :assignee_id
+          = form.hidden_field :assignee_id
           = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
             placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+        = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
     .form-group.issue-milestone
       = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
       .col-sm-10{ class: ("col-lg-8" if has_due_date) }
         .issuable-form-select-holder
-          = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+          = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
     .form-group
       - has_labels = @labels && @labels.any?
       = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index a93cbd1041f6d966815dedafbfc03fc037b623ea..8af3bd597c5a273b86c7d21870fb9f059ed3f56c 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -13,6 +13,6 @@
   - class_prefix = dom_class(issuables).pluralize
   %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
     = render partial: 'shared/milestones/issuable',
-             collection: issuables.sort_by(&:position),
+             collection: issuables.order_position_asc,
              as: :issuable,
              locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index d27fba805a332c82cf92120c15ffc00d27608b74..78079f633d5ff78bb7552c254be9f2442bb3b0b8 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -6,14 +6,15 @@
 
     .milestone-stats-and-buttons
       .milestone-stats
-        %span.milestone-stat.with-drilldown
-          %strong= milestone.issues_visible_to_user(current_user).size
-          issues:
-        %span.milestone-stat
-          %strong= milestone.issues_visible_to_user(current_user).opened.size
-          open and
-          %strong= milestone.issues_visible_to_user(current_user).closed.size
-          closed
+        - if !project || can?(current_user, :read_issue, project)
+          %span.milestone-stat.with-drilldown
+            %strong= milestone.issues_visible_to_user(current_user).size
+            issues:
+          %span.milestone-stat
+            %strong= milestone.issues_visible_to_user(current_user).opened.size
+            open and
+            %strong= milestone.issues_visible_to_user(current_user).closed.size
+            closed
         %span.milestone-stat.with-drilldown
           %strong= milestone.merge_requests.size
           merge requests:
@@ -32,10 +33,12 @@
 
       .milestone-progress-buttons
         %span.tab-issues-buttons
-          - if project && can?(current_user, :create_issue, project)
-            = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
-              New Issue
-          = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
+          - if project
+            - if can?(current_user, :create_issue, project)
+              = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
+                New Issue
+            - if can?(current_user, :read_issue, project)
+              = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
         %span.tab-merge-requests-buttons.hidden
           = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
 
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index c8f2319d95aa95afd3a9148893f82789688278ac..a0e9ec46220b021335bd79b1c64e607be6e316f9 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -1,12 +1,18 @@
 %ul.nav-links.no-top.no-bottom
-  %li.active
-    = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
-      Issues
-      %span.badge= milestone.issues_visible_to_user(current_user).size
-  %li
-    = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
-      Merge Requests
-      %span.badge= milestone.merge_requests.size
+  - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+    %li.active
+      = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+        Issues
+        %span.badge= milestone.issues_visible_to_user(current_user).size
+    %li
+      = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+        Merge Requests
+        %span.badge= milestone.merge_requests.size
+  - else
+    %li.active
+      = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+        Merge Requests
+        %span.badge= milestone.merge_requests.size
   %li
     = link_to '#tab-participants', 'data-toggle' => 'tab' do
       Participants
@@ -20,10 +26,14 @@
 - show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
 
 .tab-content.milestone-content
-  .tab-pane.active#tab-issues
-    = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
-  .tab-pane#tab-merge-requests
-    = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+  - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+    .tab-pane.active#tab-issues
+      = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+    .tab-pane#tab-merge-requests
+      = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+  - else
+    .tab-pane.active#tab-merge-requests
+      = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
   .tab-pane#tab-participants
     = render 'shared/milestones/participants_tab', users: milestone.participants
   .tab-pane#tab-labels
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index c19697802ce8cdc006f38a503f699dc470be6c25..2d25b8aad62c69ee32a9223eb44c2e3eae09b2aa 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,4 @@
 - @sort ||= sort_value_recently_updated
-- personal = params[:personal]
-- archived = params[:archived]
-- shared = params[:shared]
-- namespace_id = params[:namespace_id]
 .dropdown
   - toggle_text = projects_sort_options_hash[@sort]
   = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
@@ -11,32 +7,32 @@
       Sort by
     - projects_sort_options_hash.each do |value, title|
       %li
-        = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do
+        = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do
           = title
 
     %li.divider
     %li
-      = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
+      = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
         Hide archived projects
     %li
-      = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
+      = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do
         Show archived projects
     - if current_user
       %li.divider
       %li
-        = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do
+        = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do
           Owned by anyone
       %li
-        = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
+        = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do
           Owned by me
       - if @group && @group.shared_projects.present?
         %li.divider
         %li
-          = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do
+          = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
             All projects
         %li
-          = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do
+          = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
             Hide shared projects
         %li
-          = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do
+          = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
             Hide group projects
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 3a9dd37dc7d8a53c4b875d7db7658c71f7a48523..c57282c57424b05ccbfaa48f4d49cb8a225a785c 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -8,7 +8,7 @@
 - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
 - remote = false unless local_assigns[:remote] == true
 
-.projects-list-holder
+.js-projects-list-holder
   - if projects.any?
     %ul.projects-list.content-list
       - projects.each_with_index do |project, i|
@@ -25,6 +25,3 @@
     = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
   - else
     .nothing-here-block No projects found
-
-:javascript
-  ProjectsList.init();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4a27965754d137859cc98accbd04a45f7e566336..df21857e1ad4c6a23bc80aeb63de5fa874bc834c 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -6,17 +6,16 @@
 - css_class = '' unless local_assigns[:css_class]
 - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
 - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(project.commit.status) if project.commit.try(:status)
+- cache_key = project_list_cache_key(project)
 
 %li.project-row{ class: css_class }
   = cache(cache_key) do
     .controls
       - if project.archived
         %span.label.label-warning archived
-      - if project.commit.try(:status)
+      - if project.pipeline_status.has_status?
         %span
-          = render_commit_status(project.commit)
+          = render_project_pipeline_status(project.pipeline_status)
       - if forks
         %span
           = icon('code-fork')
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b89194bcc677c0b6bc5bfcfbbc8c73ce63e6804d
--- /dev/null
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -0,0 +1,23 @@
+= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+  = search_field_tag :name, params[:name],
+    placeholder: 'Filter by name...',
+    class: 'project-filter-form-field form-control input-short js-projects-list-filter',
+    spellcheck: false,
+    id: 'project-filter-form-field',
+    tabindex: "2",
+    autofocus: local_assigns[:autofocus]
+
+  - if local_assigns[:icon]
+    = icon("search", class: "search-icon")
+
+  - if params[:sort].present?
+    = hidden_field_tag :sort, params[:sort]
+
+  - if params[:personal].present?
+    = hidden_field_tag :personal, params[:personal]
+
+  - if params[:archived].present?
+    = hidden_field_tag :archived, params[:archived]
+
+  - if params[:visibility_level].present?
+    = hidden_field_tag :visibility_level, params[:visibility_level]
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c279a0d88461e919b33811125a45933f10aad25b
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_create.html.haml
@@ -0,0 +1,8 @@
+.dropdown-page-two.dropdown-new-branch
+  = dropdown_title('Create new branch', back: true)
+  = dropdown_content do
+    %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
+      %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+        Create
+      %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+        Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..9bf78d10878f613d5d71ec246705f2681b2490b5
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_default.html.haml
@@ -0,0 +1,10 @@
+.dropdown-page-one
+  = dropdown_title "Select branch"
+  = dropdown_filter "Search branches"
+  = dropdown_content
+  = dropdown_loading
+  = dropdown_footer do
+    %ul.dropdown-footer-list
+      %li
+        %a.create-new-branch.dropdown-toggle-page{ href: "#" }
+          Create new branch
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index ad5c0c2d8c80f6662aaffa25b50352154198ac62..74f71e6cbd1c40095c1ad13dda978ef303368981 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,7 +1,25 @@
-- unless @snippet.content.empty?
+.js-file-title.file-title-flex-parent
+  .file-header-content
+    = blob_icon @snippet.mode, @snippet.path
+
+    %strong.file-title-name
+      = @snippet.path
+
+    = copy_file_path_button(@snippet.path)
+
+  .file-actions.hidden-xs
+    .btn-group{ role: "group" }<
+      = copy_blob_content_button(@snippet)
+      = open_raw_file_button(raw_path)
+
+      - if defined?(download_path) && download_path
+        = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
+
+- if @snippet.content.empty?
+  .file-content.code
+    .nothing-here-block Empty file
+- else
   - if markup?(@snippet.file_name)
-    %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } }
-      = @snippet.content
     .file-content.wiki
       - if gitlab_markdown?(@snippet.file_name)
         = preserve(markdown_field(@snippet, :content))
@@ -9,6 +27,3 @@
         = render_markup(@snippet.file_name, @snippet.content)
   - else
     = render 'shared/file_highlight', blob: @snippet
-- else
-  .file-content.code
-    .nothing-here-block Empty file
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index e7f7db732237da83b736ecddd5f6a53ecadfd30b..0296597b29471821d4cd6ceabb87a20a31ff0679 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -3,7 +3,7 @@
   = page_specific_javascript_bundle_tag('snippet')
 
 .snippet-form-holder
-  = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
+  = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f|
     = form_errors(@snippet)
 
     .form-group
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 970afbe6b64a505e072ee1ad246647caae3f9af7..da9fb755a364a92443f05eba5fa328da48041669 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,13 +3,7 @@
 = render 'shared/snippets/header'
 
 %article.file-holder.snippet-file-content
-  .js-file-title.file-title
-    = blob_icon 0, @snippet.file_name
-    = @snippet.file_name
-    .file-actions
-      = 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 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet)
 
-= 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
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dc2fea450bd5d18943649789c79e6d5b4e5739d0..dc9a3b0d0df69741178e1680faaa2ee220e09b58 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,7 +1,7 @@
 - page_title       @user.name
 - page_description @user.bio
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_bundle_tag('lib_d3')
+  = page_specific_javascript_bundle_tag('common_d3')
   = page_specific_javascript_bundle_tag('users')
 - header_title     @user.name, user_path(@user)
 - @no_container = true
@@ -24,17 +24,16 @@
           = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
             title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
             = icon('exclamation-circle')
-      - if current_user
-        = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
-          = icon('rss')
-        - if current_user.admin?
-          = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
-            data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
-            = icon('users')
+      = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do
+        = icon('rss')
+      - if current_user && current_user.admin?
+        = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+          data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+          = icon('users')
 
     .profile-header
       .avatar-holder
-        = link_to avatar_icon(@user, 400), target: '_blank' do
+        = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
           = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
 
       .user-info
@@ -98,6 +97,7 @@
           Snippets
 
   %div{ class: container_class }
+    .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
     .tab-content
       #activity.tab-pane
         .row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 0e20df506a371d5765c5ae83e17b204cf0b9de76..13207a8bc71b8284e2586a1c0a7c9bb282180f9f 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -10,7 +10,7 @@ class AuthorizedProjectsWorker
   end
 
   def self.bulk_perform_async(args_list)
-    Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+    Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
   end
 
   def perform(user_id)
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa05fce65de38baf8707fb11099d87e9..0000000000000000000000000000000000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
-  include Sidekiq::Worker
-  include BuildQueue
-
-  def perform(build_id, recipients, push_data)
-    recipients.each do |recipient|
-      begin
-        case push_data['build_status']
-        when 'success'
-          Notify.build_success_email(build_id, recipient).deliver_now
-        when 'failed'
-          Notify.build_fail_email(build_id, recipient).deliver_now
-        end
-      # These are input errors and won't be corrected even if Sidekiq retries
-      rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
-        logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
-      end
-    end
-  end
-end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 7e44b24174358f269bc40e48b44950560a9bb9c8..c9658b3fe17c458fbee1da4548a7763c24fd9bee 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -120,8 +120,8 @@ class IrkerWorker
   end
 
   def compare_url(data, repo_path)
-    sha1 = Commit::truncate_sha(data['before'])
-    sha2 = Commit::truncate_sha(data['after'])
+    sha1 = Commit.truncate_sha(data['before'])
+    sha2 = Commit.truncate_sha(data['after'])
     compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
     compare_url += "/#{sha1}...#{sha2}"
     colorize_url compare_url
@@ -129,7 +129,7 @@ class IrkerWorker
 
   def send_one_commit(project, hook_attrs, repo_name, branch)
     commit = commit_from_id project, hook_attrs['id']
-    sha = colorize_sha Commit::truncate_sha(hook_attrs['id'])
+    sha = colorize_sha Commit.truncate_sha(hook_attrs['id'])
     author = hook_attrs['author']['name']
     files = colorize_nb_files(files_count commit)
     title = commit.title
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 2fff6b0105d1949c868517da72dbc6b8656545ea..2cd87895c5597b37137db549b88456694679ff10 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,8 +3,8 @@ class PostReceive
   include DedicatedSidekiqQueue
 
   def perform(repo_path, identifier, changes)
-    if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
-      repo_path.gsub!(path[1].to_s, "")
+    if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) }
+      repo_path.gsub!(repository_storage[1]['path'].to_s, "")
     else
       log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
     end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
deleted file mode 100644
index b70df5a1afaf061ad3077d3a161c7f0bf75d010f..0000000000000000000000000000000000000000
--- a/app/workers/stuck_ci_builds_worker.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class StuckCiBuildsWorker
-  include Sidekiq::Worker
-  include CronjobQueue
-
-  BUILD_STUCK_TIMEOUT = 1.day
-
-  def perform
-    Rails.logger.info 'Cleaning stuck builds'
-
-    builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
-    builds.find_each(batch_size: 50).each do |build|
-      Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
-      build.drop
-    end
-
-    # Update builds that failed to drop
-    builds.update_all(status: 'failed')
-  end
-end
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae8c980c9e45d92b8324a1a99bc2bb9625d2a4fa
--- /dev/null
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -0,0 +1,59 @@
+class StuckCiJobsWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'.freeze
+
+  BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
+  BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
+  BUILD_PENDING_STUCK_TIMEOUT = 1.hour
+
+  def perform
+    return unless try_obtain_lease
+
+    Rails.logger.info "#{self.class}: Cleaning stuck builds"
+
+    drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT
+    drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT
+    drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT
+
+    remove_lease
+  end
+
+  private
+
+  def try_obtain_lease
+    @uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
+  end
+
+  def remove_lease
+    Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
+  end
+
+  def drop(status, timeout)
+    search(status, timeout) do |build|
+      drop_build :outdated, build, status, timeout
+    end
+  end
+
+  def drop_stuck(status, timeout)
+    search(status, timeout) do |build|
+      return unless build.stuck?
+      drop_build :stuck, build, status, timeout
+    end
+  end
+
+  def search(status, timeout)
+    builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
+    builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
+      yield(build)
+    end
+  end
+
+  def drop_build(type, build, status, timeout)
+    Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
+    Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
+      b.drop
+    end
+  end
+end
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e43bbe35de916dee79d9ae5e327894a409a3ce1b
--- /dev/null
+++ b/app/workers/system_hook_push_worker.rb
@@ -0,0 +1,8 @@
+class SystemHookPushWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  def perform(push_data, hook_id)
+    SystemHooksService.new.execute_hooks(push_data, hook_id)
+  end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index acc4d8581361168025ba6f4ed6d91c1ee3e69295..89ae17cef37053cb34e0e547d020b45a5d25756c 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -10,8 +10,5 @@ class UpdateMergeRequestsWorker
     return unless user
 
     MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
-
-    push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, [])
-    SystemHooksService.new.execute_hooks(push_data, :push_hooks)
   end
 end
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..78931f1258fbeddc37a1f23dd470b8e8efa68437
--- /dev/null
+++ b/app/workers/upload_checksum_worker.rb
@@ -0,0 +1,12 @@
+class UploadChecksumWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  def perform(upload_id)
+    upload = Upload.find(upload_id)
+    upload.calculate_checksum
+    upload.save!
+  rescue ActiveRecord::RecordNotFound
+    Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping")
+  end
+end
diff --git a/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4a1a199673caf8ff2cb01de880baef196f5858cd
--- /dev/null
+++ b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
@@ -0,0 +1,4 @@
+---
+title: Deleting a user doesn't delete issues they've created/are assigned to
+merge_request: 7393
+author:
diff --git a/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1b7e294bd67bb2b57784700ce7129319463d8d6a
--- /dev/null
+++ b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
@@ -0,0 +1,4 @@
+---
+title: "GET 'projects/:id/repository/commits' endpoint improvements"
+merge_request: 9679
+author: George Andrinopoulos, Jordan Ryan Reuter
diff --git a/changelogs/unreleased/14748-runner-version-in-admin-views.yml b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2478a81c82496dba903e5a7430b2a3295911f10b
--- /dev/null
+++ b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
@@ -0,0 +1,4 @@
+---
+title: Add runner version to /admin/runners view
+merge_request: 8733
+author: Jonathon Reinhart
diff --git a/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f247fe35439ecff99ca2ec343b8ac28fbae978cc
--- /dev/null
+++ b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
@@ -0,0 +1,4 @@
+---
+title: Remove remnants of git annex support.
+merge_request:
+author:
diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml
deleted file mode 100644
index 12f2998d1c8584abdb35ce7623cbf79f5af5e0d5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/17662-rename-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere
-merge_request: 8787
-author:
diff --git a/changelogs/unreleased/18962-update-issues-button-jumps.yml b/changelogs/unreleased/18962-update-issues-button-jumps.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7be136ac4ff5b77dc0ce9066c1c26606e80fbedd
--- /dev/null
+++ b/changelogs/unreleased/18962-update-issues-button-jumps.yml
@@ -0,0 +1,4 @@
+---
+title: Align bulk update issues button to the right
+merge_request:
+author:
diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fa89d94e0f38c3e6d67bd0af396528deaa76d4e0
--- /dev/null
+++ b/changelogs/unreleased/1937-https-clone-url-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add the Username to the HTTP(S) clone URL of a Repository
+merge_request: 9347
+author: Jan Christophersen
diff --git a/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eceb2b9fac6fae03afcfc3573905b45041c04a18
--- /dev/null
+++ b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Hide issue info when project issues are disabled
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
new file mode 100644
index 0000000000000000000000000000000000000000..199f1edec8b82c41602d88a85733c109a96ddca2
--- /dev/null
+++ b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
@@ -0,0 +1,4 @@
+---
+title: Update permalink/blame buttons with line number fragment hash
+merge_request:
+author:
diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
deleted file mode 100644
index 965d0648adf49f0ae73af1d39c1b9965e04dd108..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb
-merge_request:
-author:
diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
deleted file mode 100644
index eda872049fdb1bae9d453d4a61aa868d6c14e0a3..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added labels empty state
-merge_request: 7443
-author: 
diff --git a/changelogs/unreleased/21451-allow-disable-mr-link.yml b/changelogs/unreleased/21451-allow-disable-mr-link.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef99970a7a2ee3d5e7be8c6aa512a07e03e91726
--- /dev/null
+++ b/changelogs/unreleased/21451-allow-disable-mr-link.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to disable Merge Request URL on push
+merge_request: 9663
+author: Alex Sanford
diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
deleted file mode 100644
index bd6c9d7521e0ac3a40d60245ee4d1a00e34f3322..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/21518_recaptcha_spam_issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use reCaptcha when an issue is identified as a spam
-merge_request: 8846
-author:
diff --git a/changelogs/unreleased/21605-allow-html5-details.yml b/changelogs/unreleased/21605-allow-html5-details.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b0c654783d96704d8f1b97836c2308a98b1a9a9e
--- /dev/null
+++ b/changelogs/unreleased/21605-allow-html5-details.yml
@@ -0,0 +1,4 @@
+---
+title: SanitizationFilter allows html5 details and summary tags
+merge_request: 6568
+author:
diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml
deleted file mode 100644
index f43c1925ad0955deda2b1a041f3989ea74781ec4..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/22007-unify-projects-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unify projects search by removing /projects/:search endpoint
-merge_request: 8877
-author:
diff --git a/changelogs/unreleased/22466-task-list-alignment.yml b/changelogs/unreleased/22466-task-list-alignment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6e6ccb873ec56e2cf7eb5b6d7b40831ab1a50499
--- /dev/null
+++ b/changelogs/unreleased/22466-task-list-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Align task list checkboxes
+merge_request: 6487
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/22562-todos-filters.yml b/changelogs/unreleased/22562-todos-filters.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9cca138744a6d4750e55cd56c0c8fc9c01b35028
--- /dev/null
+++ b/changelogs/unreleased/22562-todos-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort dropdown reflow issue
+merge_request: 9533
+author: Jarkko Tuunanen
diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
deleted file mode 100644
index 2c6883bcf7b5d7c7859b8f30f18cccfe6e0d89f8..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow creating protected branches when user can merge to such branch
-merge_request: 8458
-author:
diff --git a/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a53e7d77c16b313da26da8798392f6339b4a8556
--- /dev/null
+++ b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml
@@ -0,0 +1,4 @@
+---
+title: Add spec for todo with target_type Commit
+merge_request: 9351
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
deleted file mode 100644
index 57106e8c676b320d43179f1547ee3150c7dbfcb6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/22974-trigger-service-events-through-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds service trigger events to api
-merge_request: 8324
-author:
diff --git a/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f7c856040e03bcb3d513155f37ee8b1496e66dd3
--- /dev/null
+++ b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
@@ -0,0 +1,4 @@
+---
+title: Make Git history follow renames again by performing the --skip in Ruby
+merge_request:
+author:
diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
deleted file mode 100644
index 268be6b9b83424a30574a19e3f3a2062ee4c9da2..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs
-merge_request: 8056
-author: twonegatives
diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml
deleted file mode 100644
index dde8b2d18151db995a1b72a84b54ad029c2a95f3..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/23634-remove-project-grouping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't group issues by project on group-level and dashboard issue indexes.
-merge_request: 8111
-author: Bernardo Castro
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
deleted file mode 100644
index 587ef4f9a73424fdcda333a1205a24f94a56d7bc..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix disable storing of sensitive information when importing a new repo
-merge_request: 8885
-author: Bernard Pietraga
diff --git a/changelogs/unreleased/23948-assign-to-me.yml b/changelogs/unreleased/23948-assign-to-me.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d73aa92b0e9762bc8cf2a2fd6f4be48df21ab636
--- /dev/null
+++ b/changelogs/unreleased/23948-assign-to-me.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add Assign to me link to Merge Request and Issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/23993-drop-ci_projects.yml b/changelogs/unreleased/23993-drop-ci_projects.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee9cf774e37fdcdc69371ff639b4778cf1ba000e
--- /dev/null
+++ b/changelogs/unreleased/23993-drop-ci_projects.yml
@@ -0,0 +1,6 @@
+---
+title: Drop unused ci_projects table and some unused project_id columns,
+  then rename gl_project_id to project_id. Stop exporting job trace when
+  exporting projects.
+merge_request: 9378
+author: David Wagner
diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bcc6c6957a1f369da36c25bfd4ca0f72849209ad
--- /dev/null
+++ b/changelogs/unreleased/24137-issuable-permalink.yml
@@ -0,0 +1,4 @@
+---
+title: Link issuable reference to itself in meta-header
+merge_request: 9641
+author: mhasbini
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
deleted file mode 100644
index 14e80cacbfbca956ff45df3716e1ead08c3af798..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24147-delete-env-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds back ability to stop all environments
-merge_request: 7379
-author:
diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c57ffed6b45f73f6cb7d76be381b1760ea91a41e
--- /dev/null
+++ b/changelogs/unreleased/24166-close-builds-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent builds dropdown to close when the user clicks in a build
+merge_request:
+author:
diff --git a/changelogs/unreleased/24421-personal-milestone-count-badges.yml b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8bbc1ed2dde8f4e6b8dffe022216df996c942c20
--- /dev/null
+++ b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
@@ -0,0 +1,4 @@
+---
+title: Add dashboard and group milestones count badges
+merge_request: 9836
+author: Alex Braha Stoll
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
new file mode 100644
index 0000000000000000000000000000000000000000..31c66b2a978fef180ea99e1ad5c942b9e31da1d8
--- /dev/null
+++ b/changelogs/unreleased/24501-new-file-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New file from interface on existing branch
+merge_request: 8427
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
deleted file mode 100644
index fd671d04a9f50b5cc5b4aef20ca0be0972ee68e5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Force new password after password reset via API
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/24683-sidebar-spinners.yml b/changelogs/unreleased/24683-sidebar-spinners.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3fec273152fcf94afadf624787d1227bc3203e1c
--- /dev/null
+++ b/changelogs/unreleased/24683-sidebar-spinners.yml
@@ -0,0 +1,4 @@
+---
+title: hide loading spinners for server-rendered sidebar fields
+merge_request:
+author:
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
deleted file mode 100644
index 13de5db5e410427cd622d759e6ed936b1eafe394..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24716-fix-ctrl-click-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Ctrl+Click support for Todos and Merge Request page tabs
-merge_request: 8898
-author:
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
deleted file mode 100644
index b735fb576498787cbb5ad318128212dc7af30cb1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor MergeRequests::BuildService
-merge_request: 8462
-author: Rydkin Maxim
diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
deleted file mode 100644
index be66c370f36986a9a18b66494d484573d3170157..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Allows to search within project by commit hash'
-merge_request: 
-author: YarNayar
diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml
deleted file mode 100644
index de35cad3dd69a8ba26a17667bd569f35b9ffe961..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/24923_nested_tasks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix nested tasks in ordered list
-merge_request: 8626
-author:
diff --git a/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3b90466e3afc51692e16487baabdb54591069fdf
--- /dev/null
+++ b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml
@@ -0,0 +1,4 @@
+---
+title: Fix typo in Gitlab config file
+merge_request: 9702
+author: medied
diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
deleted file mode 100644
index d35ad0be0dbabe5c2df384595e459794ca43caf6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username
-merge_request:
-author:
diff --git a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
deleted file mode 100644
index 56e03a4869270cf1eb52676cf292567db7b65063..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent removal of input fields if it is the parent dropdown element
-merge_request: 8397
-author:
diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
deleted file mode 100644
index 50a5c879446aac2a315c1a72b994f0c26234f4f0..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove flash warning from login page
-merge_request: 8864
-author: Gerald J. Padilla
diff --git a/changelogs/unreleased/25367-add-impersonation-token.yml b/changelogs/unreleased/25367-add-impersonation-token.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4a30f960036574900b540179b092fa04e74a6ac4
--- /dev/null
+++ b/changelogs/unreleased/25367-add-impersonation-token.yml
@@ -0,0 +1,4 @@
+---
+title: Manage user personal access tokens through api and add impersonation tokens
+merge_request: 9099
+author: Simon Vocella
diff --git a/changelogs/unreleased/25437-just-emoji.yml b/changelogs/unreleased/25437-just-emoji.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ceb81a47f2d2f6a28ce53788736629fbc386e2d5
--- /dev/null
+++ b/changelogs/unreleased/25437-just-emoji.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce /award slash command; Allow posting of just an emoji in comment
+merge_request: 9382
+author: mhasbini
diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml
deleted file mode 100644
index dac90eaa34df75e6a373adbf960098df45b4b798..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25460-replace-word-users-with-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace word user with member
-merge_request: 8872
-author:
diff --git a/changelogs/unreleased/25503_issues_finder_performance.yml b/changelogs/unreleased/25503_issues_finder_performance.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87964269c6d564a28a3fe9de40bed27adb3199a7
--- /dev/null
+++ b/changelogs/unreleased/25503_issues_finder_performance.yml
@@ -0,0 +1,4 @@
+---
+title: Filter by projects in the end of search
+merge_request: 9030
+author:
diff --git a/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5b755a8bc32576a412554e7da1f242fb0dd332d9
--- /dev/null
+++ b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Create a new issue for a single discussion in a Merge Request
+merge_request: 8266
+author: Bob Van Landuyt
diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
deleted file mode 100644
index d7f950d7be93b25e4e1b513c3c0586227d072315..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove turbolinks.
-merge_request: !8570
-author:
diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
deleted file mode 100644
index f74e9fa8b6dbf52b72f5d480e81683378bd162b0..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update pipeline and commit links when CI status is updated
-merge_request: 8351
-author:
diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
deleted file mode 100644
index 9506692dd4063e3665632a14b2b57c90318ad609..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert pipeline action icons to svg to have them propperly positioned
-merge_request:
-author:
diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
new file mode 100644
index 0000000000000000000000000000000000000000..580d1074aa76fa75953aced669845270e36793bc
--- /dev/null
+++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to create issue for failing build
+merge_request: 9391
+author: Alex Sanford
diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
deleted file mode 100644
index e67a9c0da152b1f89852d61e59947ac9ca5e26d4..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove rogue scrollbars for issue comments with inline elements
-merge_request:
-author:
diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml
deleted file mode 100644
index fc3f1af5b611575e6210b4ce74de8a9f05fc698a..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26059-segoe-ui-vertical.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Align Segoe UI label text
-merge_request:
-author:
diff --git a/changelogs/unreleased/26068_tasklist_issue.yml b/changelogs/unreleased/26068_tasklist_issue.yml
deleted file mode 100644
index c938351b8a739dca2fb4a3293788f32e249d7f87..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26068_tasklist_issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don’t count tasks that are not defined as list items correctly
-merge_request: 8526
-author:
diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
deleted file mode 100644
index b2f5294d380f56662c2832d5269de47e08835afd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add sorting pipeline for a commit
-merge_request: 8319
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/26136-list-repository-tree-api-doc.yml b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..85d8bc6ca8ae5a062d3497f46aa16f0fae7eb6d5
--- /dev/null
+++ b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Make documentation of list repository tree API call more detailed
+merge_request: 9532
+author: Marius Kleiner
diff --git a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml b/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
deleted file mode 100644
index 565672917b262699012b245e88bc378261939e2c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Color + and - signs in diffs to increase code legibility
-merge_request:
-author:
diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb00d46ea1fda8e530a119316b7ed043a9610a83
--- /dev/null
+++ b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
@@ -0,0 +1,4 @@
+---
+title: Don't show links to tag a commit for users that are not permitted
+merge_request: 8407
+author:
diff --git a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
new file mode 100644
index 0000000000000000000000000000000000000000..827224abf5adb81a602afe46bd7e6287ecc09291
--- /dev/null
+++ b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
@@ -0,0 +1,4 @@
+---
+title: Changed dropdown style slightly
+merge_request:
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c44f3d4d3dc44f96776641109ec1a281417476dc
--- /dev/null
+++ b/changelogs/unreleased/26236-monospace-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Change gfm textarea to use monospace font
+merge_request:
+author:
diff --git a/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6ee8e5724bc9d3d2d4ea5f5b913e47ada4a70545
--- /dev/null
+++ b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml
@@ -0,0 +1,4 @@
+---
+title: Show public RSS feeds to anonymous users
+merge_request: 9596
+author: Michael Kozono
diff --git a/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce888baa32fe5855a1ca15b68235c1ad7b6550b8
--- /dev/null
+++ b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Clean-up Groups navigation order
+merge_request: 9309
+author:
diff --git a/changelogs/unreleased/26348-cleanup-navigation-order.yml b/changelogs/unreleased/26348-cleanup-navigation-order.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5324f9e02536f8d98aa3cf24f01541d806bf030
--- /dev/null
+++ b/changelogs/unreleased/26348-cleanup-navigation-order.yml
@@ -0,0 +1,4 @@
+---
+title: Clean-up Project navigation order
+merge_request: 9272
+author:
diff --git a/changelogs/unreleased/26371-native-emojis-v3-code.yml b/changelogs/unreleased/26371-native-emojis-v3-code.yml
new file mode 100644
index 0000000000000000000000000000000000000000..883467114902a0f6eaae87e7c4b80b8dd97d797e
--- /dev/null
+++ b/changelogs/unreleased/26371-native-emojis-v3-code.yml
@@ -0,0 +1,4 @@
+---
+title: Use native unicode emojis
+merge_request:
+author:
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
deleted file mode 100644
index fb5274e5253375346716ad37ad26a4b3784f835f..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve button accessibility on pipelines page
-merge_request: 8561
-author:
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
deleted file mode 100644
index 351c53bd07679e5fb3e0f2f3374297c23dd6b81b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26447-fix-tab-list-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix tab index order on branch commits list page
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
deleted file mode 100644
index 87ae8233c4a92a3b87caed69b6ed677ec36b5034..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Sort by Recent Sign-in in Admin Area
-merge_request: 8637
-author: Poornima M
diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e82cbf00cfbbcfc5f31d1a41e945555026598a84
--- /dev/null
+++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Strip reference prefixes on branch creation
+merge_request: 8498
+author: Matthieu Tardy
diff --git a/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6fc4615dab8cfb94b8de1c1a86c3ba29b0f4defa
--- /dev/null
+++ b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Combined deploy keys, push rules, protect branches and mirror repository settings options into a single one called
+  Repository
+merge_request:
+author:
diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
deleted file mode 100644
index 31f1812c6f805d2337484b022e440c81a920ba12..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add hover style to copy icon on commit page header
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26790-label-color-todos.yml b/changelogs/unreleased/26790-label-color-todos.yml
new file mode 100644
index 0000000000000000000000000000000000000000..74084473d81269d94bc23511694ed0a45215a381
--- /dev/null
+++ b/changelogs/unreleased/26790-label-color-todos.yml
@@ -0,0 +1,4 @@
+---
+title: fix background color for labels mention in todo
+merge_request: 9155
+author: mhasbini
diff --git a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml
deleted file mode 100644
index 182a9ae126b5589c8d0b243a5913a350632c04bd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: prevent diff unfolding link from appearing when there are no more lines to
-  show
-merge_request: 8761
-author:
diff --git a/changelogs/unreleased/26847-api-pipelines-use-basic.yml b/changelogs/unreleased/26847-api-pipelines-use-basic.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2034a4ba08074584387b684615b70f2a5ff09525
--- /dev/null
+++ b/changelogs/unreleased/26847-api-pipelines-use-basic.yml
@@ -0,0 +1,4 @@
+---
+title: Expose pipelines as PipelineBasic `api/v3/projects/:id/pipelines`
+merge_request: 8875
+author:
diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
deleted file mode 100644
index fb65b068b23ebc15f1792ffaa0a58b81e7fe1fbd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26852-fix-slug-for-openshift.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG
-merge_request: 8638
-author:
diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
deleted file mode 100644
index 8dfabf87c2a43d6c7734543402869a06c91a02aa..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove hover animation from row elements
-merge_request:
-author:
diff --git a/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3d6400cba76173e71853cdd452a0a0ac5316869b
--- /dev/null
+++ b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
@@ -0,0 +1,4 @@
+---
+title: Add all available statuses to scope filter for project builds endpoint
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml b/changelogs/unreleased/26881-backup-fails-if-data-changes.yml
deleted file mode 100644
index 00bf105560b9673e5053c5d68d37aca2bb32d3cd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `copy` backup strategy to combat file changed errors
-merge_request: 8728
-author:
diff --git a/changelogs/unreleased/26900-pipelines-tabs.yml b/changelogs/unreleased/26900-pipelines-tabs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f08514c621fa0ebedcddb4d7a9daa0e273830b95
--- /dev/null
+++ b/changelogs/unreleased/26900-pipelines-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Adds Pending and Finished tabs to pipelines page
+merge_request:
+author:
diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
deleted file mode 100644
index ea567437ac29ffeb8fa014ba4c15645300b7478d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes hover cursor on pipeline pagenation
-merge_request: 9003
-author:
diff --git a/changelogs/unreleased/26947-build-status-self-link.yml b/changelogs/unreleased/26947-build-status-self-link.yml
deleted file mode 100644
index 15c5821874eff063869fb062fc115754606cc05a..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26947-build-status-self-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add link verification to badge partial in order to render a badge without a link
-merge_request: 8740
-author:
diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
deleted file mode 100644
index c5c57af5aaf90f68290f2bc46ff5d0c8b1597383..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve pipeline status icon linking in widgets
-merge_request:
-author:
diff --git a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml b/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
deleted file mode 100644
index 7cb5e4b273d4a6490f9e6d6a64634db9c7ecbaa1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix commit title bar and repository view copy clipboard button order on last commit in repository view
-merge_request:
-author:
diff --git a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml b/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
deleted file mode 100644
index f0301c849b69ec8feccf8446e28b12581217d281..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix mini-pipeline stage tooltip text wrapping
-merge_request:
-author:
diff --git a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml b/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
deleted file mode 100644
index b5584749098c5ee19d6d7187c44db58137e087ff..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent copying of line numbers in parallel diff view
-merge_request: 8706
-author:
diff --git a/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a9f70e339c04ecb02bf0b0a4fa6d96460d26157f
--- /dev/null
+++ b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
@@ -0,0 +1,4 @@
+---
+title: Add housekeeping endpoint for Projects API
+merge_request: 9421
+author:
diff --git a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
new file mode 100644
index 0000000000000000000000000000000000000000..44aae486574afdf9f090a8f15d2d6cb365d4a8ba
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo mark all as done to Todos
+merge_request: 9890
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2e6c10a6bfe59277ded4b14f565f8fde74ade45e
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo to Todos in the Done tab
+merge_request: 8782
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee236310a717f089a2f7bbc628379de26dc43638
--- /dev/null
+++ b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
@@ -0,0 +1,4 @@
+---
+title: API: Add environment stop action
+merge_request: 8808
+author:
diff --git a/changelogs/unreleased/27174-filter-filters.yml b/changelogs/unreleased/27174-filter-filters.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0da1e4d5d3b4807de0e83f16733c33cd01aed5f1
--- /dev/null
+++ b/changelogs/unreleased/27174-filter-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent filtering issues by multiple Milestones or Authors
+merge_request:
+author:
diff --git a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml b/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
deleted file mode 100644
index 52406bba46456667998bb0591cad777b827e1493..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Updated builds info link on the project settings page
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
deleted file mode 100644
index 3f902fb324eeafbdcbcd9debeffd481a0f033cdd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27240-make-progress-bars-consistent.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 27240 Make progress bars consistent
-merge_request:
-author:
diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ea52a70e89e527c31de6f4ce4248ed410cd5f42
--- /dev/null
+++ b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
@@ -0,0 +1,4 @@
+---
+title: Include time tracking attributes in webhooks payload
+merge_request: 9942
+author:
diff --git a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml b/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
deleted file mode 100644
index 9456251025b890e0ea3fce1b30c8ede2050c43db..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fixed small mini pipeline graph line glitch
-merge_request: 8804
-author:
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
deleted file mode 100644
index 293aab67d396c284d3f2616e12c6acff2a48cff1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unify MR diff file button style
-merge_request: 8874
-author:
diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
deleted file mode 100644
index 502927cd160d29563e8b85ae234b6686909b7fbc..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only render hr when user can't archive project.
-merge_request: !8917
-author:
diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
deleted file mode 100644
index 79316abbaf7e72f97d721204528b91d7415f2ea6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline graph vertical spacing in Firefox and Safari
-merge_request: 8886
-author:
diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml
deleted file mode 100644
index 191b530aee896eb151bdab1e6c8c32afcaae4e4d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27352-search-label-filter-header.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 27352-search-label-filter-header
-merge_request:
-author:
diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..62cac9bbbd3883c968241464575f702ad94e0f41
--- /dev/null
+++ b/changelogs/unreleased/27354-navigation-new-button.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add the New Project button in nav bar
+merge_request:
+author:
diff --git a/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a116c68ad87c0baaf1c0e0f188eceec9b555a207
--- /dev/null
+++ b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
@@ -0,0 +1,4 @@
+---
+title: Speed up project dashboard by caching pipeline status and eager loading routes
+merge_request: 9903
+author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
deleted file mode 100644
index f3ce1709518a7bc82b014e0dc68da427b42ed9e5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include :author, :project, and :target in Event.with_associations
-merge_request:
-author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
deleted file mode 100644
index 3f6d922f2a0bf8e0f92ef4517d8b851b47c3a5a1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't instantiate AR objects in Event.in_projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
deleted file mode 100644
index dc400d65006354727409c15e6ed50b4de0b0b18b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27484-environment-show-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't capitalize environment name in show page
-merge_request:
-author:
diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml
deleted file mode 100644
index 5135ff0fd60a37259f3bf2864e5f036c43b10ee8..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27488-fix-jwt-version.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update and pin the `jwt` gem to ~> 1.5.6
-merge_request:
-author:
diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml
deleted file mode 100644
index 798c01f3238763e5d34441ac3e0f27d6e6697764..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27494-environment-list-column-headers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
-merge_request:
-author:
diff --git a/changelogs/unreleased/27501-api-use-visibility-everywhere.yml b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f1b70687878d3350cc6656f596859ba1b8796c59
--- /dev/null
+++ b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml
@@ -0,0 +1,4 @@
+---
+title: "API: Use `visibility` as string parameter everywhere"
+merge_request: 9337
+author:
diff --git a/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3050b072863d0b537af3dceb2acc93cca3178581
--- /dev/null
+++ b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml
@@ -0,0 +1,4 @@
+---
+title: Option to prevent signing in from multiple ips
+merge_request: 8998
+author:
diff --git a/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml b/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4ef2b23aaa654e8af84569ca83f968a81b06f71
--- /dev/null
+++ b/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Make stuck builds detection more performant
+merge_request: 9025
+author:
diff --git a/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4436b4bee685e695728f45db0298f1cf182fade7
--- /dev/null
+++ b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes job dropdown action throws error in js console
+merge_request: 9182
+author:
diff --git a/changelogs/unreleased/27532_api_changes.yml b/changelogs/unreleased/27532_api_changes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..778469d5a862613d796659091da5c4f841c2c0fe
--- /dev/null
+++ b/changelogs/unreleased/27532_api_changes.yml
@@ -0,0 +1,4 @@
+---
+title: Use iids as filter parameter
+merge_request: 9096
+author:
diff --git a/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c738af77047e3d286aaaa09426b7b710bd261e1
--- /dev/null
+++ b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_assignee_spec
+merge_request: 9711
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
deleted file mode 100644
index a5bb37ec8a942bb9ecbecda5cf13475990921a29..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes flickering of avatar border in mention dropdown
-merge_request: 8950
-author:
diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml
deleted file mode 100644
index 958621a43a147ea8d3cb00774e9aec5f3dc003cb..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27632_fix_mr_widget_url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix MR widget url
-merge_request: 8989
-author:
diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
deleted file mode 100644
index 0531ef2c038bf3367fcd63f570a1bf75d891bc11..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Layer award emoji dropdown over the right sidebar
-merge_request: 9004
-author:
diff --git a/changelogs/unreleased/27656-doc-ci-enable-ci.yml b/changelogs/unreleased/27656-doc-ci-enable-ci.yml
deleted file mode 100644
index e6315d683d4b1d6770b93e934ad9269326a7ad05..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27656-doc-ci-enable-ci.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update doc for enabling or disabling GitLab CI
-merge_request: 8965
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/27762-add-default-artifacts-expiration.yml b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..27fa77ed04dd161b65927fe8ca8a48cd33acbe05
--- /dev/null
+++ b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
@@ -0,0 +1,4 @@
+---
+title: Add admin setting for default artifacts expiration
+merge_request: 9219
+author:
diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
deleted file mode 100644
index aa89d9f985020e4ed34518619940a63901827886..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Give ci status text on pipeline graph a better font-weight
-merge_request:
-author:
diff --git a/changelogs/unreleased/27778-a11y-sidebar.yml b/changelogs/unreleased/27778-a11y-sidebar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb37d7fdb35241662affa7ce6d1a106a2de78ba2
--- /dev/null
+++ b/changelogs/unreleased/27778-a11y-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Improves a11y in sidebar by adding aria-hidden attributes in i tags and by
+  fixing two broken aria-hidden attributes
+merge_request:
+author:
diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
deleted file mode 100644
index ee2431869f059808b3537baa574031f5e550798d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27822-default-bulk-assign-labels.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add default labels to bulk assign dropdowns
-merge_request:
-author:
diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87b1f0c5572e903717855e10ad4e5e1c71a2cc8d
--- /dev/null
+++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml
@@ -0,0 +1,4 @@
+---
+title: Enhanced filter issues layout for better mobile experiance
+merge_request: 9280
+author: Pratik Borsadiya
diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
deleted file mode 100644
index 89e2bdc69bc3fc934cff6b0e49d6eed242dacf52..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only return target project's comments for a commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
deleted file mode 100644
index 4251754618b8888cc229fe3bca9841578dff95e6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes Pipelines table is not showing branch name for commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml
deleted file mode 100644
index 52b7e96682df52ff08b44db4fc89986030b442a3..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Trigger autocomplete after selecting a slash command
-merge_request: 9117
-author:
diff --git a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml b/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
deleted file mode 100644
index 79a54429ee88ea6fb7fec82bef5365a9761d86d2..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix regression where cmd-click stopped working for todos and merge request
-  tabs
-merge_request:
-author:
diff --git a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml b/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
deleted file mode 100644
index f7bdb62b7f32fe04ef5b982034109f30e98ec6c5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix stray pipelines API request when showing MR
-merge_request:
-author:
diff --git a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
deleted file mode 100644
index b7505e284018353ab080c5a9c9f1bc6fa50a36f5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Merge request pipelines displays JSON
-merge_request:
-author:
diff --git a/changelogs/unreleased/27934-left-align-logo.yml b/changelogs/unreleased/27934-left-align-logo.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d4e5e1694651d1cb4f61ef98d179799b1460dae7
--- /dev/null
+++ b/changelogs/unreleased/27934-left-align-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Left align logo
+merge_request:
+author:
diff --git a/changelogs/unreleased/27934-left-align-nav.yml b/changelogs/unreleased/27934-left-align-nav.yml
deleted file mode 100644
index 6c45e4ce1753e372aa48c219ec151d80c5d1be9c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27934-left-align-nav.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Left align navigation
-merge_request:
-author:
diff --git a/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
new file mode 100644
index 0000000000000000000000000000000000000000..adc129d8dca615bf263431f3b311ef6e3b684547
--- /dev/null
+++ b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
@@ -0,0 +1,4 @@
+---
+title: Uploaded files which content can change now require revalidation on each page load
+merge_request: 9453
+author:
diff --git a/changelogs/unreleased/27939-fix-current-build-arrow.yml b/changelogs/unreleased/27939-fix-current-build-arrow.yml
deleted file mode 100644
index 280ab090f2c28f14641c9d20987cdca9cf3ba1c6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27939-fix-current-build-arrow.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix current build arrow indicator
-merge_request:
-author:
diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
deleted file mode 100644
index fcbd48b0357f832633ba0a70e08ed078caf518a5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix contribution activity alignment
-merge_request:
-author:
diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
deleted file mode 100644
index 1dfabd3813be7db7216245867704eb160d1eec4a..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add space between text and loading icon in Megre Request Widget
-merge_request: 9119
-author:
diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
deleted file mode 100644
index d9f78db4bec32817dc4292899f67f257fbca6747..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show Pipeline(not Job) in MR desktop notification
-merge_request:
-author:
diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml
deleted file mode 100644
index ba418d86433a5616d6c5cd35f6997f885938bf3b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27963-tooltips-jobs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix tooltips in mini pipeline graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml b/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
deleted file mode 100644
index 6fa13395a7d51d97ed65f5dcbc7da854fe91f2f6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display loading indicator when filtering ref switcher dropdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/27978-improve-task-list-ux.yml b/changelogs/unreleased/27978-improve-task-list-ux.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a6bd99da82ee8edad59d8dba52299400788317df
--- /dev/null
+++ b/changelogs/unreleased/27978-improve-task-list-ux.yml
@@ -0,0 +1,4 @@
+---
+title: Only add a newline in the Markdown Editor if the current line is not empty
+merge_request: 9455
+author: Jan Christophersen
diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
deleted file mode 100644
index e4287d6276cac17f83d17a6e4b2f6cadd4a1e31b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show pipeline graph in MR widget if there are any stages
-merge_request:
-author:
diff --git a/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c6ba9572f2671d5092abd6b8446f0fefb534bfd0
--- /dev/null
+++ b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add `requirements: { id: /.+/ }` for all projects and groups namespaced API
+  routes'
+merge_request: 9944
+author:
diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml
deleted file mode 100644
index 703d34a5ede94d2a09cb1fed59eb9f03400ef815..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/27991-success-with-warnings-caret.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix icon colors in merge request widget mini graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml
new file mode 100644
index 0000000000000000000000000000000000000000..06bb669ceace8794c3e3b9af582290bd91f63ed1
--- /dev/null
+++ b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml
@@ -0,0 +1,4 @@
+---
+title: Default to subtle MR mege button until CI status is available
+merge_request:
+author:
diff --git a/changelogs/unreleased/28019-make-builds-show-faster.yml b/changelogs/unreleased/28019-make-builds-show-faster.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bbfea0e4c88b9a6762597ef7d54bceee5802548a
--- /dev/null
+++ b/changelogs/unreleased/28019-make-builds-show-faster.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid calling Build#trace_with_state for performance
+merge_request: 9149
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
deleted file mode 100644
index be2a0afbc5269b76d67f45d78a0d2e9c5b116ec2..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve blockquote formatting in notification emails
-merge_request:
-author:
diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6f4082d7684f305ab08a3db1acedbae9a4bb620f
--- /dev/null
+++ b/changelogs/unreleased/28030-infinite-offset.yml
@@ -0,0 +1,4 @@
+---
+title: allow offset query parameter for infinite list pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/28032-tooltips-file-name.yml b/changelogs/unreleased/28032-tooltips-file-name.yml
deleted file mode 100644
index 9fe11e7c2b63fd7d1786d5576eba8c967d200acf..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/28032-tooltips-file-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds container to tooltip in order to make it work with overflow:hidden in
-  parent element
-merge_request:
-author:
diff --git a/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0e826a67f8e781204f867e58690645220b4a9f7
--- /dev/null
+++ b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
@@ -0,0 +1,4 @@
+---
+title: Only show public emails in atom feeds
+merge_request:
+author:
diff --git a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml
deleted file mode 100644
index 1b2e678bbeda9542f562e8497992cbdc8ee0eadc..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Restore pagination to admin abuse reports
-merge_request:
-author:
diff --git a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml b/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml
deleted file mode 100644
index d70b5ef8fd560495adcfb631326ed79e9dab737b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Spam check and reCAPTCHA improvements
-merge_request:
-author:
diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
new file mode 100644
index 0000000000000000000000000000000000000000..feca38ff083ad461b5c8d4e53283a99c832f161d
--- /dev/null
+++ b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Use toggle button to expand / collapse mulit-nested groups
+merge_request: 9501
+author:
diff --git a/changelogs/unreleased/28257-issues-iids.yml b/changelogs/unreleased/28257-issues-iids.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a85504a8de681dd045b1db3becbe84b587b051f
--- /dev/null
+++ b/changelogs/unreleased/28257-issues-iids.yml
@@ -0,0 +1,4 @@
+---
+title: API issues - support filtering by iids
+merge_request:
+author:
diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6e3cd8a60d8440d8964feab07bed137f712bec6e
--- /dev/null
+++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Document U2F limitations with multiple URLs
+merge_request: 9300
+author:
diff --git a/changelogs/unreleased/28357-colon-search.yml b/changelogs/unreleased/28357-colon-search.yml
deleted file mode 100644
index 4bbb0dc12b23610a7c09afb2bd28854570eafc35..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/28357-colon-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow searching issues for strings containing colons
-merge_request:
-author:
diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
new file mode 100644
index 0000000000000000000000000000000000000000..faf1e89ed9408297650b1f922585b3355b51bbd6
--- /dev/null
+++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
@@ -0,0 +1,4 @@
+---
+title: Remove markup that was showing in tooltip for renamed files
+merge_request: 9374
+author:
diff --git a/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6fc89fd91dd8d0ba3004baae6bf8fea55066c18f
--- /dev/null
+++ b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes includes line number during unfold copy n paste in parallel diff view
+merge_request: 9365
+author:
diff --git a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd94b3fe66346e78fab0b380a27ed8dc51a3e0b7
--- /dev/null
+++ b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong message on starred projects filtering
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28410-dropdown-styling.yml b/changelogs/unreleased/28410-dropdown-styling.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2a7af1dd6e894f2c242628c3fb19a7bd6af84b1e
--- /dev/null
+++ b/changelogs/unreleased/28410-dropdown-styling.yml
@@ -0,0 +1,4 @@
+---
+title: Add badges to global dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/28447-hybrid-repository-storages.yml b/changelogs/unreleased/28447-hybrid-repository-storages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..00dfc5781b9b851f3a0e0371122fa7db293d6783
--- /dev/null
+++ b/changelogs/unreleased/28447-hybrid-repository-storages.yml
@@ -0,0 +1,4 @@
+---
+title: Update storage settings to allow extra values per repository storage
+merge_request: 9597
+author:
diff --git a/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..196a9b788ea9d058c85577ac8d50b338199b1b72
--- /dev/null
+++ b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
@@ -0,0 +1,4 @@
+---
+title: test compiling production assets and generate webpack bundle report in CI
+merge_request: 9396
+author:
diff --git a/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dbbe8a192043129ec6a37092431602a1be462ba6
--- /dev/null
+++ b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
@@ -0,0 +1,4 @@
+---
+title: Present GitLab version for each V3 to V4 API change on v3_to_v4.md
+merge_request:
+author:
diff --git a/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
new file mode 100644
index 0000000000000000000000000000000000000000..67dbc30e76086db5f946195c033f6752f516e05b
--- /dev/null
+++ b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
@@ -0,0 +1,4 @@
+---
+title: Adds pipeline mini-graph to system information box in Commit View
+merge_request:
+author:
diff --git a/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
new file mode 100644
index 0000000000000000000000000000000000000000..660a881e094e3bd74628c4f88b17764d8eabe7ba
--- /dev/null
+++ b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes large file name tooltip cutoff in diff header
+merge_request: 9529
+author:
diff --git a/changelogs/unreleased/28516-default-kubernetes-namespace.yml b/changelogs/unreleased/28516-default-kubernetes-namespace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9fa5c681a532f61ccb41c27d7d249401144ce13a
--- /dev/null
+++ b/changelogs/unreleased/28516-default-kubernetes-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Make a default namespace of Kubernetes service to contain project ID
+merge_request:
+author:
diff --git a/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eda5764c13ead8bb305bf2576ba7ef1ee36bcdc2
--- /dev/null
+++ b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
@@ -0,0 +1,4 @@
+---
+title: Document when current coverage configuration option was introduced
+merge_request: 9443
+author:
diff --git a/changelogs/unreleased/28538-restore-nav-shortcuts.yml b/changelogs/unreleased/28538-restore-nav-shortcuts.yml
new file mode 100644
index 0000000000000000000000000000000000000000..07b39cd50d1c87918c666143c9e72209e8f87ab3
--- /dev/null
+++ b/changelogs/unreleased/28538-restore-nav-shortcuts.yml
@@ -0,0 +1,4 @@
+---
+title: Restore keyboard shortcuts for "Activity" and "Charts"
+merge_request: 9680
+author:
diff --git a/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ada726c9048032c8bfd35ae28d7f823010166638
--- /dev/null
+++ b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml
@@ -0,0 +1,4 @@
+---
+title: Narrow environment payload by using basic project details resource
+merge_request:
+author:
diff --git a/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bff996172f33d9bc4fe21baa4d82efb1b5f3620a
--- /dev/null
+++ b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
@@ -0,0 +1,4 @@
+---
+title: Update account view to display new username
+merge_request:
+author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b592766bf3c768f2275672d4f9328918b566bac
--- /dev/null
+++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dismissable error close is not visible enough
+merge_request: 9516
+author:
diff --git a/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e38e5d0db5b550a04ca550a89bfc8644929984ca
--- /dev/null
+++ b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Improve grammar in GitLab flow documentation
+merge_request: 9552
+author: infogrind
diff --git a/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b8dba0b599365918a733b58b6b8ae89336e21a61
--- /dev/null
+++ b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml
@@ -0,0 +1,4 @@
+---
+title: Set max height to screen height for Zen mode
+merge_request: 9667
+author:
diff --git a/changelogs/unreleased/28723-consistent-handling-indexof.yml b/changelogs/unreleased/28723-consistent-handling-indexof.yml
new file mode 100644
index 0000000000000000000000000000000000000000..95d6181d5fa115972ba1af48e141aa13cfe8f034
--- /dev/null
+++ b/changelogs/unreleased/28723-consistent-handling-indexof.yml
@@ -0,0 +1,4 @@
+---
+title: Keep consistent in handling indexOf results
+merge_request: 9531
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml
new file mode 100644
index 0000000000000000000000000000000000000000..38ff6b97b2bfa68772798c44af0a386bfeca9be2
--- /dev/null
+++ b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure archive download is only one directory deep
+merge_request: 9616
+author:
diff --git a/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0016253e32ef0ed6209aa84bae1bc6622ae988b8
--- /dev/null
+++ b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml
@@ -0,0 +1,4 @@
+---
+title: Enable filtering milestones by search criteria in the API
+merge_request: 9606
+author:
diff --git a/changelogs/unreleased/28835-jobs-head.yml b/changelogs/unreleased/28835-jobs-head.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1580cfb19baa411a1e6f62a50f66e99c8cafa789
--- /dev/null
+++ b/changelogs/unreleased/28835-jobs-head.yml
@@ -0,0 +1,4 @@
+---
+title: Fix jobs table header height
+merge_request:
+author:
diff --git a/changelogs/unreleased/28837-remove-help-duplicate.yml b/changelogs/unreleased/28837-remove-help-duplicate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b10012456637b44472ceba5db8f6ec127bf348fb
--- /dev/null
+++ b/changelogs/unreleased/28837-remove-help-duplicate.yml
@@ -0,0 +1,4 @@
+---
+title: Remove help link from right dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7c64783cbd0cbcdbd9dc00c6a3ea4cce5a11b3d6
--- /dev/null
+++ b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml
@@ -0,0 +1,4 @@
+---
+title: Add filter param for project membership for current_user in API v4
+merge_request:
+author:
diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0177394aa0f9b545aeaf682a3f4a0f604e6306ea
--- /dev/null
+++ b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
@@ -0,0 +1,4 @@
+---
+title: Order milestone issues by position ascending in api
+merge_request: 9635
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
new file mode 100644
index 0000000000000000000000000000000000000000..114a14ec2df651b21b6d4f37be8a0a0d2b1bfa3a
--- /dev/null
+++ b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge request even if target branch is not specified in query
+  params
+merge_request: 9968
+author:
diff --git a/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9ba33af010cfbf55d25d0143de0913d3c99a6567
--- /dev/null
+++ b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml
@@ -0,0 +1,4 @@
+---
+title: Highlight line number if specified on diff pages when page loads
+merge_request: 9664
+author:
diff --git a/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
new file mode 100644
index 0000000000000000000000000000000000000000..48e62f8f70dc83165d2652ab89b6748d64a5d9e6
--- /dev/null
+++ b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
@@ -0,0 +1,4 @@
+---
+title: Fix json response in branches controller
+merge_request: 9710
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28935-make-logo-smaller.yml b/changelogs/unreleased/28935-make-logo-smaller.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef79fc7d2123865a42f5b5cb34a5d18d9aaa2873
--- /dev/null
+++ b/changelogs/unreleased/28935-make-logo-smaller.yml
@@ -0,0 +1,4 @@
+---
+title: Decrease tanuki logo size
+merge_request:
+author:
diff --git a/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
new file mode 100644
index 0000000000000000000000000000000000000000..26989c149582e696f642aaed41b29036cb14912b
--- /dev/null
+++ b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
@@ -0,0 +1,4 @@
+---
+title: When viewing old wiki page version, edit button should be disabled
+merge_request: 9966 
+author: TM Lee
diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f869249c22bb32d64aac261034d0587ebb238af6
--- /dev/null
+++ b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
@@ -0,0 +1,4 @@
+---
+title: Fix create issue form buttons are misaligned on mobile
+merge_request: 9706
+author: TM Lee
diff --git a/changelogs/unreleased/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6d08db3d55db3da563d85caeeaa1ef03c81e0761
--- /dev/null
+++ b/changelogs/unreleased/29034-fix-github-importer.yml
@@ -0,0 +1,4 @@
+---
+title: Fix name colision when importing GitHub pull requests from forked repositories
+merge_request: 9719
+author:
diff --git a/changelogs/unreleased/29046-fix-github-importer-open-prs.yml b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d279c269f9437962bbc116f147c73e17e03dd08b
--- /dev/null
+++ b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix GitHub Import deleting branches for open PRs from a fork
+merge_request: 9758
+author:
diff --git a/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0de7754badcd688c1505c55edeb898ecda166f7c
--- /dev/null
+++ b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
@@ -0,0 +1,4 @@
+---
+title: Make authorized projects worker use a specific queue instead of the default one
+merge_request: 9813
+author:
diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad0c513f525611748d55e1c68888eb5fdc7b6479
--- /dev/null
+++ b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_milestone_spec.rb
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29189-discussion-button.yml b/changelogs/unreleased/29189-discussion-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eea963621178a03b557f132e7c2cfc6bbd7d2a0d
--- /dev/null
+++ b/changelogs/unreleased/29189-discussion-button.yml
@@ -0,0 +1,4 @@
+---
+title: Fix alignment of resolve button
+merge_request:
+author:
diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8e3a71f875b36aecc4a43c8bead2507ebf00a43
--- /dev/null
+++ b/changelogs/unreleased/29209-sign-up-form-name.yml
@@ -0,0 +1,4 @@
+---
+title: Change label for name on sign up form
+merge_request:
+author:
diff --git a/changelogs/unreleased/29263-merge-button-color.yml b/changelogs/unreleased/29263-merge-button-color.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2d0625483a419b6925a2a43bb86ae9058cd1fd99
--- /dev/null
+++ b/changelogs/unreleased/29263-merge-button-color.yml
@@ -0,0 +1,4 @@
+---
+title: ensure MR widget dropdown is same color as button
+merge_request:
+author:
diff --git a/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dabf9968c5bcdf26506e6fc627a5ca15f7bc1654
--- /dev/null
+++ b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Add custom attributes in factories
+merge_request: 9892
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
new file mode 100644
index 0000000000000000000000000000000000000000..85be36f7902a8b17ae42b27b65d77e2b93225641
--- /dev/null
+++ b/changelogs/unreleased/29405-fix-project-wiki-update.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Project Wiki update
+merge_request: 9990
+author: Dongqing Hu
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..61ffb64fa8f9e29bca37585d667ac24e0043518c
--- /dev/null
+++ b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trigger webhook for ref with a dot
+merge_request: 10001
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..23a32d2c11a9ea28976fdeb4a8ad254727938226
--- /dev/null
+++ b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Display full project name with namespace upon deletion
+merge_request:
+author:
diff --git a/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d0a04b0a130fc75689bf57738880bf71d443345a
--- /dev/null
+++ b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
@@ -0,0 +1,4 @@
+---
+title: Fix archive prefix bug for refs containing dots
+merge_request:
+author:
diff --git a/changelogs/unreleased/29604-v3-fix-branch-creation.yml b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
new file mode 100644
index 0000000000000000000000000000000000000000..25687e8be97fcd95faefd4c8c7c32c7111d49c9c
--- /dev/null
+++ b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
@@ -0,0 +1,4 @@
+---
+title: Use "branch_name" instead "branch" on V3 branch creation API
+merge_request:
+author:
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15d7b9dcafb5517b07dfb1c13ec78baf23b3946e
--- /dev/null
+++ b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to some Branch API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/3440-remove-hsts-header.yml b/changelogs/unreleased/3440-remove-hsts-header.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0310e733f4e18f822a0454e819a1c480b972d4a8
--- /dev/null
+++ b/changelogs/unreleased/3440-remove-hsts-header.yml
@@ -0,0 +1,4 @@
+---
+title: Stop setting Strict-Transport-Securty header from within the app
+merge_request:
+author:
diff --git a/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4a4932288b4a23a74ba185185c22c078c8f25120
--- /dev/null
+++ b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
@@ -0,0 +1,4 @@
+---
+title: Return 202 with JSON body on async removals on V4 API
+merge_request:
+author:
diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
deleted file mode 100644
index 11d1f55172b1d2a08b0a2e58fca9fa1d695d04a7..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix notifications when set at group level
-merge_request: 6813
-author: Alexandre Maia
diff --git a/changelogs/unreleased/6073_project_api.yml b/changelogs/unreleased/6073_project_api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fd6792a406e3792fd130d8904e3bf5eac7eed01a
--- /dev/null
+++ b/changelogs/unreleased/6073_project_api.yml
@@ -0,0 +1,4 @@
+---
+title: 'API project create: Make name or path required'
+merge_request: 9416
+author:
diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml
deleted file mode 100644
index 75502e139e7b3cb64e921b35605ce0297a5dc273..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/8-15-stable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure export files are removed after a namespace is deleted
-merge_request: 
-author: 
diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml
deleted file mode 100644
index 136d2108c63473afd5f5b85ceedb181fea6522f3..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/8082-permalink-to-file.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `y` keyboard shortcut to move to file permalink
-merge_request:
-author:
diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml
deleted file mode 100644
index 2f0f18872576a16204ee568e090f565f4e49ac73..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/9-0-api-changes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
-merge_request: 8967
-author:
diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
deleted file mode 100644
index 9fd6ea5bc52d3a5ea9096b4651bd59a8e8cc0239..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds /target_branch slash command functionality for merge requests
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..307b7ec73592c0e9f68d1d4f8f433eedc0349f72
--- /dev/null
+++ b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent more than one issue tracker to be active for the same project
+merge_request:
+author: luisdgs19
diff --git a/changelogs/unreleased/add-blob-copy-button.yml b/changelogs/unreleased/add-blob-copy-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..946723e523bae4422d33ae4dae259a59de927754
--- /dev/null
+++ b/changelogs/unreleased/add-blob-copy-button.yml
@@ -0,0 +1,4 @@
+---
+title: Add copy button to blob header and use icon for Raw button
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d10e4cb7c87f4d1a1718e53e826637f6dd727e54
--- /dev/null
+++ b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
@@ -0,0 +1,4 @@
+---
+title: Add filtered search visual tokens
+merge_request: 8969
+author:
diff --git a/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..66d5bb63734260c85600db2e21de497188ef08d3
--- /dev/null
+++ b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml
@@ -0,0 +1,4 @@
+---
+title: Add frequently used emojis back to awards menu
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-git-version-to-system-info.yml b/changelogs/unreleased/add-git-version-to-system-info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2827fcec28dc7ce010c5e176378f36008eaef563
--- /dev/null
+++ b/changelogs/unreleased/add-git-version-to-system-info.yml
@@ -0,0 +1,4 @@
+---
+title: Add git version to gitlab:env:info
+merge_request: 9128
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ae1e3c7a7af156461526c394081accd3ea940af
--- /dev/null
+++ b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml
@@ -0,0 +1,4 @@
+---
+title: Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM
+merge_request: 9398
+author:
diff --git a/changelogs/unreleased/add-labels-to-issue-hook.yml b/changelogs/unreleased/add-labels-to-issue-hook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..967430ee09f9bb33a9ee35869971c921fccecafd
--- /dev/null
+++ b/changelogs/unreleased/add-labels-to-issue-hook.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels array to the issue web hook returned object
+merge_request: 9972
+author:
diff --git a/changelogs/unreleased/add-pipeline-triggers.yml b/changelogs/unreleased/add-pipeline-triggers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..81b11da0bb241c431c1ea85ba4197c92f4067f50
--- /dev/null
+++ b/changelogs/unreleased/add-pipeline-triggers.yml
@@ -0,0 +1,4 @@
+---
+title: Add pipeline trigger API with user permissions
+merge_request: 9277
+author:
diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8087aa6296c66a4bd3fbcf33bd3c64812d085a2b
--- /dev/null
+++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml
@@ -0,0 +1,4 @@
+---
+title: Add merge request count to each issue on issues list
+merge_request: 9252
+author: blackst0ne
diff --git a/changelogs/unreleased/add_project_update_hook.yml b/changelogs/unreleased/add_project_update_hook.yml
deleted file mode 100644
index 915c953884386584cedf7815cc5cddbd4fa16156..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/add_project_update_hook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add system hook for when a project is updated (other than rename/transfer)
-merge_request: 5711
-author: Tommy Beadle
diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
new file mode 100644
index 0000000000000000000000000000000000000000..088f13357960cf3c55a6e1c4936e74103c990efe
--- /dev/null
+++ b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
@@ -0,0 +1,4 @@
+---
+title: Add quick submit for snippet forms
+merge_request: 9911
+author: blackst0ne
diff --git a/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c3c877423ff27d6361f8083610c2a6e4800df507
--- /dev/null
+++ b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
@@ -0,0 +1,4 @@
+---
+title: Fix conflict resolution when files contain valid UTF-8 characters
+merge_request:
+author:
diff --git a/changelogs/unreleased/api-drop-subscribed.yml b/changelogs/unreleased/api-drop-subscribed.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2a39026b519e6aa04e43f5c05cfd21170838a481
--- /dev/null
+++ b/changelogs/unreleased/api-drop-subscribed.yml
@@ -0,0 +1,5 @@
+---
+title: Remove "subscribed" field from API responses returning list of issues or merge
+  requests
+merge_request: 9661
+author:
diff --git a/changelogs/unreleased/api-empty-return.yml b/changelogs/unreleased/api-empty-return.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7810e83eb0eb9e9aa587c289f3a3a18e9fe0f8c2
--- /dev/null
+++ b/changelogs/unreleased/api-empty-return.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 204 for all delete endpoints'
+merge_request: 9397
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-notes-entity-fields.yml b/changelogs/unreleased/api-notes-entity-fields.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f7631df31e2c0f5ed78ddf991e983895ea811346
--- /dev/null
+++ b/changelogs/unreleased/api-notes-entity-fields.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated fields Notes#upvotes and Notes#downvotes'
+merge_request: 9384
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-project-issues-404.yml b/changelogs/unreleased/api-project-issues-404.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce40395c99e28a7d87433dbf71c4f3954987af2a
--- /dev/null
+++ b/changelogs/unreleased/api-project-issues-404.yml
@@ -0,0 +1,4 @@
+---
+title: Return 404 in project issues API endpoint when project cannot be found
+merge_request: 10093
+author:
diff --git a/changelogs/unreleased/api-remove-owned-groups.yml b/changelogs/unreleased/api-remove-owned-groups.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cf0301b7fe0adfca1299755bfe2025e95fefe57e
--- /dev/null
+++ b/changelogs/unreleased/api-remove-owned-groups.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove /groups/owned endpoint'
+merge_request: 9505
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
deleted file mode 100644
index 67603bfab3bd960955de4a275f6dc93f6fad4455..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/api-remove-snippets-expires-at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove deprecated ''expires_at'' from project snippets'
-merge_request: 8723
-author: Robert Schilling
diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml
deleted file mode 100644
index fda1c3bd562a28b31eab8aff08cf4e95b70c466e..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/babel-all-the-things.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: use babel to transpile all non-vendor javascript assets regardless of file
-  extension
-merge_request: 8988
-author:
diff --git a/changelogs/unreleased/backup_storage_class.yml b/changelogs/unreleased/backup_storage_class.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fc9989fc2510b42e8850113904778a88672d8468
--- /dev/null
+++ b/changelogs/unreleased/backup_storage_class.yml
@@ -0,0 +1,4 @@
+---
+title: Add storage class configuration option for Amazon S3 remote backups
+merge_request:
+author: Jon Keys
diff --git a/changelogs/unreleased/better-priority-sorting-2.yml b/changelogs/unreleased/better-priority-sorting-2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ca0d14718dcd1045a5198155bb6f262337c7a5c6
--- /dev/null
+++ b/changelogs/unreleased/better-priority-sorting-2.yml
@@ -0,0 +1,4 @@
+---
+title: Allow filtering by all started milestones
+merge_request:
+author:
diff --git a/changelogs/unreleased/dynamic-todos-fixture.yml b/changelogs/unreleased/better-priority-sorting.yml
similarity index 50%
rename from changelogs/unreleased/dynamic-todos-fixture.yml
rename to changelogs/unreleased/better-priority-sorting.yml
index 580bc729e3c112327ccc8f28e2581d0a596b5e89..a44cd090ceb4a4ed73a89e3f5273920743e45039 100644
--- a/changelogs/unreleased/dynamic-todos-fixture.yml
+++ b/changelogs/unreleased/better-priority-sorting.yml
@@ -1,4 +1,4 @@
 ---
-title: Replace static fixture for right_sidebar_spec.js
-merge_request: 9211
-author: winniehell
+title: Allow sorting by due date and priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/branch_deletion.yml b/changelogs/unreleased/branch_deletion.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dbc9265a1fb7e5394bb90cffaf2dfacd71c0d3ce
--- /dev/null
+++ b/changelogs/unreleased/branch_deletion.yml
@@ -0,0 +1,4 @@
+---
+title: on branch deletion show loading icon and disabled the button
+merge_request: 6761
+author: wendy0402
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
deleted file mode 100644
index 77750b55e7e440da1ae174a66847e1b1a528bc7d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide version check image if there is no internet connection
-merge_request: 8355
-author: Ken Ding
diff --git a/changelogs/unreleased/bugfix-systemhook.yml b/changelogs/unreleased/bugfix-systemhook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4c4d0dcc7a29746974ec32a62fc50ec28231897b
--- /dev/null
+++ b/changelogs/unreleased/bugfix-systemhook.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bug when system hook for deploy key
+merge_request: 9796
+author: billy.lb
diff --git a/changelogs/unreleased/change_queue_weight.yml b/changelogs/unreleased/change_queue_weight.yml
deleted file mode 100644
index e4c650e8f79b11814f4a21eb61a37fb5a2f9cf30..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/change_queue_weight.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Increase process_commit queue weight from 2 to 3
-merge_request: 9326
-author: blackst0ne
diff --git a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dc315ca23672e645cf1ef3d90c99793c26a3bd13
--- /dev/null
+++ b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Added remaining_time method to milestoneish, specs and updated the milestone_helper
+  milestone_remaining_days method to correctly return the correct remaining time.
+merge_request:
+author: Michael Robinson
diff --git a/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8778fac6e9d8515e5defbeb1917eeb5077248459
--- /dev/null
+++ b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Clear ActiveRecord connections before starting Sidekiq
+merge_request:
+author:
diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml
deleted file mode 100644
index 6aa4a5664e7e592b0fba3627f0ddd2c5b8269526..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/clipboard-button-commit-sha.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: 'Copy commit SHA to clipboard'
-merge_request: 8547
diff --git a/changelogs/unreleased/commit-search-ui-fix.yml b/changelogs/unreleased/commit-search-ui-fix.yml
deleted file mode 100644
index 4a5c2cf609081e2748435a429738476478685723..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/commit-search-ui-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed commit search UI
-merge_request:
-author:
diff --git a/changelogs/unreleased/commons-chunk-plugin.yml b/changelogs/unreleased/commons-chunk-plugin.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c11ea3bbb20d1711273001df55674176714ba9a
--- /dev/null
+++ b/changelogs/unreleased/commons-chunk-plugin.yml
@@ -0,0 +1,5 @@
+---
+title: Use webpack CommonsChunkPlugin to place common javascript libraries in their
+  own bundles
+merge_request: 9647
+author:
diff --git a/changelogs/unreleased/contribution-calendar-scroll.yml b/changelogs/unreleased/contribution-calendar-scroll.yml
deleted file mode 100644
index a504d59e61c4223e5ae7a7bb2262dae970079143..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/contribution-calendar-scroll.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: contribution calendar scrolls from right to left
-merge_request:
-author:
diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml
deleted file mode 100644
index 506815a5b540edf8355c895c1f9ac35a78d9e7ed..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/cop-gem-fetcher.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Cop for gem fetched from a git source
-merge_request: 8856
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/copy-as-md.yml b/changelogs/unreleased/copy-as-md.yml
deleted file mode 100644
index 637e9dc36e29a822af3a4e51ece09346b91d253c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/copy-as-md.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
-merge_request:
-author:
diff --git a/changelogs/unreleased/create_branch_repo_less.yml b/changelogs/unreleased/create_branch_repo_less.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8b14fa3b679cf62e29731902675f03d6d41b89b
--- /dev/null
+++ b/changelogs/unreleased/create_branch_repo_less.yml
@@ -0,0 +1,4 @@
+---
+title: Creating a new branch from an issue will automatically initialize a repository if one doesn't already exist.
+merge_request:
+author:
diff --git a/changelogs/unreleased/dashboard-filter-search-keep-params.yml b/changelogs/unreleased/dashboard-filter-search-keep-params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a140715b7a20ffe879d4a91c0b9d052d089c1e90
--- /dev/null
+++ b/changelogs/unreleased/dashboard-filter-search-keep-params.yml
@@ -0,0 +1,4 @@
+---
+title: Dashboard project search keeps selected sort & filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/delete-artifacts-for-pages.yml b/changelogs/unreleased/delete-artifacts-for-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50b3dd81d600dab4e5bac1ea2d708fbd38fb29fc
--- /dev/null
+++ b/changelogs/unreleased/delete-artifacts-for-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Delete artifacts for pages unless expiry date is specified
+merge_request: 9716
+author:
diff --git a/changelogs/unreleased/diff-make-obvious-cant-comment.yml b/changelogs/unreleased/diff-make-obvious-cant-comment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2cb9594793922cc5f29c0012187cc9e9517437f4
--- /dev/null
+++ b/changelogs/unreleased/diff-make-obvious-cant-comment.yml
@@ -0,0 +1,4 @@
+---
+title: Visually show expanded diff lines cant have comments
+merge_request:
+author:
diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
deleted file mode 100644
index 6dd0d74800197f35f68d61c48f42dde54d4819c5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable automatic login after clicking email confirmation links
-merge_request: 7472
-author:
diff --git a/changelogs/unreleased/display-project-id.yml b/changelogs/unreleased/display-project-id.yml
deleted file mode 100644
index 8705ed28400cb2140f9d40728edd0fd10e33da55..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/display-project-id.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display project ID in project settings
-merge_request: 8572
-author: winniehell
diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15ae2da44a34aae4dd2546d50159e4415a16fa74
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Copy code as GFM from diffs, blobs and GFM code blocks
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-group-reference-full-name.yml b/changelogs/unreleased/dm-group-reference-full-name.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f445d955529caf20479f4552f6f5928c34aaf52f
--- /dev/null
+++ b/changelogs/unreleased/dm-group-reference-full-name.yml
@@ -0,0 +1,4 @@
+---
+title: Use full group name in GFM group reference title
+merge_request:
+author:
diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml
deleted file mode 100644
index 863e41b64136e2c88c4ae5ad7eef29a5fe491651..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/document-how-to-vue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds documentation for how to use Vue.js
-merge_request: 8866
-author:
diff --git a/changelogs/unreleased/dz-blacklist--names.yml b/changelogs/unreleased/dz-blacklist--names.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2941965002de76e78e935aea72f02aa4d859b9dd
--- /dev/null
+++ b/changelogs/unreleased/dz-blacklist--names.yml
@@ -0,0 +1,4 @@
+---
+title: Reserve few project and nested group paths that have wildcard routes associated
+merge_request: 9898
+author:
diff --git a/changelogs/unreleased/dz-change-project-view.yml b/changelogs/unreleased/dz-change-project-view.yml
new file mode 100644
index 0000000000000000000000000000000000000000..47e007a80a8d571350cb77a9c28576febd8a3796
--- /dev/null
+++ b/changelogs/unreleased/dz-change-project-view.yml
@@ -0,0 +1,4 @@
+---
+title: Change default project view for user from readme to files view
+merge_request: 9584
+author:
diff --git a/changelogs/unreleased/dz-dashboard-groups-search.yml b/changelogs/unreleased/dz-dashboard-groups-search.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c473cba774d9d8fa583f4f7c5fef4f27408fd4c7
--- /dev/null
+++ b/changelogs/unreleased/dz-dashboard-groups-search.yml
@@ -0,0 +1,4 @@
+---
+title: Add filter and sorting to dashboard groups page
+merge_request: 9619
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
deleted file mode 100644
index 8e4eb7f1fff5dc1163792a685c694d989fc8842c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/dz-nested-groups-improvements-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add read-only full_path and full_name attributes to Group API
-merge_request: 8827
-author:
diff --git a/changelogs/unreleased/dz-nested-groups-members.yml b/changelogs/unreleased/dz-nested-groups-members.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bab0c8465c20efca99e2774048232a5b9909e8f3
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-members.yml
@@ -0,0 +1,4 @@
+---
+title: Show members of parent groups on project members page
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-restrictions.yml b/changelogs/unreleased/dz-nested-groups-restrictions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2ffb6032525849da8fb84d1582d7bc27966ddc8f
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-restrictions.yml
@@ -0,0 +1,4 @@
+---
+title: Restrict nested group names to prevent ambiguous routes
+merge_request: 9738
+author:
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
deleted file mode 100644
index 5a42c98a8003c2c15757067d5ffe1a504a3428d7..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/empty-selection-reply-shortcut.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change the reply shortcut to focus the field even without a selection.
-merge_request: 8873
-author: Brian Hall
diff --git a/changelogs/unreleased/enable-snippets-by-default.yml b/changelogs/unreleased/enable-snippets-by-default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..04fa3f7bdaebb1f9f95eb539156720d0ea3b3f54
--- /dev/null
+++ b/changelogs/unreleased/enable-snippets-by-default.yml
@@ -0,0 +1,4 @@
+---
+title: Enable snippets for new projects by default
+merge_request:
+author:
diff --git a/changelogs/unreleased/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9d1c3ac7421ef69f13db247af553373f1ba95222
--- /dev/null
+++ b/changelogs/unreleased/es6-class-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Convert Issue into ES6 class
+merge_request: 9636
+author: winniehell
diff --git a/changelogs/unreleased/etag-notes-polling.yml b/changelogs/unreleased/etag-notes-polling.yml
new file mode 100644
index 0000000000000000000000000000000000000000..53990821d253748be50cd26e27f0ad7c8aecf675
--- /dev/null
+++ b/changelogs/unreleased/etag-notes-polling.yml
@@ -0,0 +1,4 @@
+---
+title: Use ETag to improve performance of issue notes polling
+merge_request: 9036
+author:
diff --git a/changelogs/unreleased/expose-pagination-headers.yml b/changelogs/unreleased/expose-pagination-headers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1b4cd43fa0688f0d9ace23aa084dd93922267b03
--- /dev/null
+++ b/changelogs/unreleased/expose-pagination-headers.yml
@@ -0,0 +1,4 @@
+---
+title: 'CORS: Whitelist pagination headers'
+merge_request: 9651
+author: Robert Schilling
diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml
deleted file mode 100644
index b5cc6bbf8b610fefd6f6b303eaf39a5837e27728..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fe-commit-mr-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use vue.js Pipelines table in commit and merge request view
-merge_request: 8844
-author:
diff --git a/changelogs/unreleased/feature-brand-logo-in-emails.yml b/changelogs/unreleased/feature-brand-logo-in-emails.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a7674b9b25e1fab470b84254264b4fd77fbb0a59
--- /dev/null
+++ b/changelogs/unreleased/feature-brand-logo-in-emails.yml
@@ -0,0 +1,4 @@
+---
+title: Brand header logo for pipeline emails
+merge_request: 9049
+author: Alexis Reigel
diff --git a/changelogs/unreleased/feature-custom-lfs.yml b/changelogs/unreleased/feature-custom-lfs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ec968386a6f4827e7535bfcecc1c085e79592061
--- /dev/null
+++ b/changelogs/unreleased/feature-custom-lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Do not show LFS object when LFS is disabled
+merge_request: 9779
+author: Christopher Bartz
diff --git a/changelogs/unreleased/feature-openid-connect.yml b/changelogs/unreleased/feature-openid-connect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e84eb7aff863906a4411be51fb34df68ed178326
--- /dev/null
+++ b/changelogs/unreleased/feature-openid-connect.yml
@@ -0,0 +1,4 @@
+---
+title: Implement OpenID Connect identity provider
+merge_request: 8018
+author: Markus Koller
diff --git a/changelogs/unreleased/feature-runner-jobs-v4-api.yml b/changelogs/unreleased/feature-runner-jobs-v4-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b24ea65266d61c0d2498b27c4c66940bd5ba0c80
--- /dev/null
+++ b/changelogs/unreleased/feature-runner-jobs-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's jobs v4 API
+merge_request: 9273
+author:
diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e646a6a17b7d8b790953db7abcb76d2f27af9ea8
--- /dev/null
+++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's registration/deletion v4 API
+merge_request: 9246
+author:
diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
deleted file mode 100644
index 5fba0332881958fcb9ba1a3dab1fded8eb7eb3df..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use warning icon in mini-graph if stage passed conditionally
-merge_request: 8503
-author:
diff --git a/changelogs/unreleased/feature-syshook_commits.yml b/changelogs/unreleased/feature-syshook_commits.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1305f5cd414fa71cb919d968f3cf5d600865797a
--- /dev/null
+++ b/changelogs/unreleased/feature-syshook_commits.yml
@@ -0,0 +1,4 @@
+---
+title: Added commit array to Syshook json
+merge_request: 9685
+author: Gabriele Pongelli
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b668d994a1a62d1729db223d60280f1c7d08bf6
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for CommitController#show
+merge_request: 9629
+author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
deleted file mode 100644
index cc72a8306951a7fc2446d18521ab88421e353f46..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-27479.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove new branch button for confidential issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-29093.yml b/changelogs/unreleased/fix-29093.yml
new file mode 100644
index 0000000000000000000000000000000000000000..791129afe9357e3eb6a9a3b1d38833675b5d8365
--- /dev/null
+++ b/changelogs/unreleased/fix-29093.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml
deleted file mode 100644
index 33b677b1f2919f9e273b63fb694db2569ec6bed5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-api-mr-permissions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't allow project guests to subscribe to merge requests through the API
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/fix-ar-connection-leaks.yml b/changelogs/unreleased/fix-ar-connection-leaks.yml
deleted file mode 100644
index 9da715560adf861d6909778ba99239f2f927a1c2..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-ar-connection-leaks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't connect in Gitlab::Database.adapter_name
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml
deleted file mode 100644
index 26003713ed443867868f35b25ae0bd59baeb6222..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-ci-build-policy.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve build policy and access abilities
-merge_request: 8711
-author:
diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml
deleted file mode 100644
index e13215f22a75235af9cf5fead67a6b515c146e5f..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-deleting-project-again.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix deleting projects with pipelines and builds
-merge_request: 8960
-author:
diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml
deleted file mode 100644
index 618170277204559c81beeeb58bb15940d06fe77e..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-depr-warn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: resolve deprecation warnings
-merge_request: 8855
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
deleted file mode 100644
index df7e3776700bca27de954c98fade3542e5b92cc1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context
-merge_request: 8981
-author:
diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4db684c40b26a65805ff4f8b9511e35a678ea1e5
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve project pipeline status caching problem on dashboard
+merge_request: 9895
+author:
diff --git a/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
new file mode 100644
index 0000000000000000000000000000000000000000..605b5f01d0e7fcd9bb68c2414fd8fdc4f34286b4
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
@@ -0,0 +1,4 @@
+---
+title: Deprecate usage of `types` configuration entry to describe CI/CD stages
+merge_request: 9766
+author:
diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6365b1a19102ad6e9a36cff1becd427276ade24b
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
@@ -0,0 +1,4 @@
+---
+title: Fix "passed with warnings" stage status on MySQL installations
+merge_request: 9802
+author:
diff --git a/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml
new file mode 100644
index 0000000000000000000000000000000000000000..71ff768a190f9f21ed8933c566617a524c580b9e
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml
@@ -0,0 +1,4 @@
+---
+title: Remove deprecated build status badge and related services
+merge_request: 9620
+author:
diff --git a/changelogs/unreleased/fix-gb-update-commit-status-api.yml b/changelogs/unreleased/fix-gb-update-commit-status-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aa4fcba4e8928d47afaf0fbf858531938c1c9508
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-update-commit-status-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix updaing commit status when using optional attributes
+merge_request: 9618
+author:
diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
deleted file mode 100644
index 81377c0c6f0814d10a2f2c1f886d3bfdf9a5afdb..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from creating notes on resources they can't access
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml
deleted file mode 100644
index e34d895570b6fd804133b1267bd0528552a94e39..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-import-encrypt-atts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ignore encrypted attributes in Import/Export
-merge_request: 
-author: 
diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml
deleted file mode 100644
index fe580af31b38d9c48a01ef4403230183ff6a2416..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-import-group-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to export project inherited group members to Import/Export
-merge_request: 8923
-author: 
diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
deleted file mode 100644
index d5f34b4b25d4da941b8cd8b8f6d1478079eef9f4..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix job to pipeline renaming
-merge_request: 9147
-author:
diff --git a/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee827b7c939cb1a70e8e388fd5d55736fa566f01
--- /dev/null
+++ b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issues mentioned but not closed for external issue trackers
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bf17a758c80792eb46d531f7fa85293ac9b68734
--- /dev/null
+++ b/changelogs/unreleased/fix-milestone-name-on-show.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Milestone name on show page
+merge_request:
+author: Raveesh
diff --git a/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a42b0db3cfc2352efbf83371feda124e88e15980
--- /dev/null
+++ b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
@@ -0,0 +1,4 @@
+---
+title: Removed d3 from the main application.js bundle
+merge_request: 10062
+author:
diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml
deleted file mode 100644
index b927279cdf4759535855198d34bf6c51af7cda26..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-references-header-parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix reply by email without sub-addressing for some clients from
-  Microsoft and Apple
-merge_request: 8620
-author:
diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml
deleted file mode 100644
index e98ac755b8804f31ad25543348624f5c34602140..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-scroll-test.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change rspec test to guarantee window is resized before visiting page
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-slow-queries-for-branches-index.yml b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5bd7003615290f4048e055e461580acd0b4d50f
--- /dev/null
+++ b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes n+1 query for tags and branches index page
+merge_request: 9905
+author:
diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
deleted file mode 100644
index c9edd1de86c89f16befdedc24be482b588ff8e06..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from deleting system deploy keys via the project deploy key API
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
deleted file mode 100644
index 4551212759f73540ee1f766a805960004bd95fe5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix_broken_diff_discussions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make MR-review-discussions more reliable
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml
deleted file mode 100644
index e09d03bb60853799e9c73fd5c50cb1219ece3723..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix incorrect sidekiq concurrency count in admin background page
-merge_request:
-author: wendy0402
diff --git a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
new file mode 100644
index 0000000000000000000000000000000000000000..414facdf7791bcb00e1fa572afd7349683e3800a
--- /dev/null
+++ b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
@@ -0,0 +1,4 @@
+---
+title: Fix xml.updated field in rss/atom feeds
+merge_request: 9889
+author: blackst0ne
diff --git a/changelogs/unreleased/fix_visibility_level.yml b/changelogs/unreleased/fix_visibility_level.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4cf649124cabe5d759c6766eae5006d1afb2b1b8
--- /dev/null
+++ b/changelogs/unreleased/fix_visibility_level.yml
@@ -0,0 +1,4 @@
+---
+title: Fix visibility level on new project page
+merge_request: 9885
+author: blackst0ne
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f353400753a5d949f1933b97241b179d64f5c32d
--- /dev/null
+++ b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'Removes UJS from pipelines tables'
+merge_request: 9929
+author:
diff --git a/changelogs/unreleased/format-timeago-date.yml b/changelogs/unreleased/format-timeago-date.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f331c34abbc4ccb34b51419af239aea4ab69a0bf
--- /dev/null
+++ b/changelogs/unreleased/format-timeago-date.yml
@@ -0,0 +1,4 @@
+---
+title: Format timeago date to short format
+merge_request:
+author:
diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml
deleted file mode 100644
index 1427e4e7624ba1d6e94a782319967bb98e2359e5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/fwn-to-find-by-full-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: replace `find_with_namespace` with `find_by_full_path`
-merge_request: 8949
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml b/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
deleted file mode 100644
index f60417d185ebfef087a8d500c24208c6c75a3c7f..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make notification_service spec DRYer by making test reusable
-merge_request: 
-author: YarNayar
diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml
deleted file mode 100644
index b2959c02c07686cc087f48f294170a6dfa4564bc..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/git_to_html_redirection.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redirect http://someproject.git to http://someproject
-merge_request:
-author: blackst0ne
diff --git a/changelogs/unreleased/gitaly-post-receive.yml b/changelogs/unreleased/gitaly-post-receive.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cf206e39084bcd3526cf533888565223a59c9f1f
--- /dev/null
+++ b/changelogs/unreleased/gitaly-post-receive.yml
@@ -0,0 +1,4 @@
+---
+title: Add internal API to notify Gitaly of post receive
+merge_request: 8983
+author:
diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml
deleted file mode 100644
index 7f372ccb42846c1566a7e7bd8e91b245f190a59f..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/go-go-gadget-webpack.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: use webpack to bundle frontend assets and use karma for frontend testing
-merge_request: 7288
-author:
diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aff1bdd957c14ba89125bf2bf24aacb263e12d57
--- /dev/null
+++ b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved the gear settings dropdown to a tab in the groups view
+merge_request:
+author:
diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml
deleted file mode 100644
index c11c2d4ede1b4be508f966a521989e8ea2ecf059..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/group-label-sidebar-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed group label links in issue/merge request sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/handle-failure-when-deleting-tags.yml b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
new file mode 100644
index 0000000000000000000000000000000000000000..99b07c5fb5fcf51b9803974fcdc0a5632b5c1f87
--- /dev/null
+++ b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
@@ -0,0 +1,4 @@
+---
+title: Display error message when deleting tag in web UI fails
+merge_request: 9906
+author:
diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml
deleted file mode 100644
index 1b0a63efa51871bdfe304060bbebf52057fe9bd9..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/hardcode-title-system-note.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure autogenerated title does not cause failing spec
-merge_request: 8963
-author: brian m. carlson
diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml
deleted file mode 100644
index 39a85e3d26176df5fe8a1d5575a0ba33e18c85d5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/improve-ci-example-php-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed composer installer script in the CI PHP example doc
-merge_request: 4342
-author: Jeffrey Cafferata
diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml
deleted file mode 100644
index 8ae3dfe079cdf3adea3b8e40fe601a73f798d431..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/improve-handleLocationHash-tests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve gl.utils.handleLocationHash tests
-merge_request:
-author:
diff --git a/changelogs/unreleased/introduce-pipeline-triggers.yml b/changelogs/unreleased/introduce-pipeline-triggers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce5a230d48f51d133c79a6ad38c61689e92a85e8
--- /dev/null
+++ b/changelogs/unreleased/introduce-pipeline-triggers.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce Pipeline Triggers that are user-aware
+merge_request:
+author:
diff --git a/changelogs/unreleased/issuable-sidebar-bug.yml b/changelogs/unreleased/issuable-sidebar-bug.yml
deleted file mode 100644
index 4086292eb89273923ec069173639c48094d0ff8c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issuable-sidebar-bug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed Issuable sidebar not closing on smaller/mobile sized screens
-merge_request: 
-author: 
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
deleted file mode 100644
index 60da1c14702b2e8cd5b0ac5264a326340c9e1326..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue-20428.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to define a coverage regex in the .gitlab-ci.yml
-merge_request: 7447
-author: Leandro Camargo
diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b02be70c470ae39f22db91ebb41ec97da16e5f0a
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-new-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Added new filtered search bar to issue boards
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-descrpiption-spinner-off.yml b/changelogs/unreleased/issue-descrpiption-spinner-off.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87104d09804a160c92a80dca53c765ff57e62b29
--- /dev/null
+++ b/changelogs/unreleased/issue-descrpiption-spinner-off.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed loading spinner position on issue template toggle
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
deleted file mode 100644
index 263af75b9e952d10b8e5ae7cc02e4892a60faa34..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue-sidebar-empty-assignee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resets assignee dropdown when sidebar is open
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml
new file mode 100644
index 0000000000000000000000000000000000000000..abf4a609932c445b7fa3c6bc300d129f3781de06
--- /dev/null
+++ b/changelogs/unreleased/issue-tags-layout.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'New Tag' layout on Tags page
+merge_request:
+author: Robert Marcano
diff --git a/changelogs/unreleased/issue_16834.yml b/changelogs/unreleased/issue_16834.yml
new file mode 100644
index 0000000000000000000000000000000000000000..06175579ac3abef78e6b756652304603c797555a
--- /dev/null
+++ b/changelogs/unreleased/issue_16834.yml
@@ -0,0 +1,4 @@
+---
+title: Update API endpoints for raw files
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
deleted file mode 100644
index 5dea1493f23e77452d48c21da1ea4bb014e8abc8..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue_19262.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disallow system notes for closed issuables
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_23317.yml b/changelogs/unreleased/issue_23317.yml
deleted file mode 100644
index 788ae159f5eededbb689c9b74adbdfa0e176d8d0..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue_23317.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix timezone on issue boards due date
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml
new file mode 100644
index 0000000000000000000000000000000000000000..916e47d36a941c97d28cb29fadfba632234b749e
--- /dev/null
+++ b/changelogs/unreleased/issue_24815.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issuable stale object error handler for js when updating tasklists
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_25112.yml b/changelogs/unreleased/issue_25112.yml
deleted file mode 100644
index c43d2732b9acfc331cce20b71fc386430999c37d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue_25112.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable invalid service templates
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27211.yml b/changelogs/unreleased/issue_27211.yml
deleted file mode 100644
index ad48fec5d853eb366aaa9f6c1fe85c0b36d28bac..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue_27211.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unused js response from refs controller
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a7e04f7ca7265919f7aef1dd1ace68fd9883fb7
--- /dev/null
+++ b/changelogs/unreleased/issue_27212.yml
@@ -0,0 +1,4 @@
+---
+title: Add closed_at field to issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_28051_2.yml b/changelogs/unreleased/issue_28051_2.yml
deleted file mode 100644
index 8cc32ad84937e340a9a15942f0a606de3a45e124..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/issue_28051_2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use default branch as target_branch when parameter is missing
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_29449.yml b/changelogs/unreleased/issue_29449.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3556f22b08085b9859ad875cc8e7b46594a26cf3
--- /dev/null
+++ b/changelogs/unreleased/issue_29449.yml
@@ -0,0 +1,4 @@
+---
+title: Remove whitespace in group links
+merge_request: 9947
+author: Xurxo Méndez Pérez
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
deleted file mode 100644
index ee4a43a93db3b701bf5de374d7cbfdcda9301417..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/jej-pages-picked-from-ee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added GitLab Pages to CE
-merge_request: 8463
-author:
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
deleted file mode 100644
index 2ab997bf42027079a612850b83cf491fa485fac6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/label-promotion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Project labels can now be promoted to group labels"
-merge_request: 7242
-author: Olaf Tomalka
diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml
deleted file mode 100644
index 60f62d7691be3da0e2d543a5af82e5f0e831bfa1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/lfs-noauth-public-repo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support unauthenticated LFS object downloads for public projects
-merge_request: 8824
-author: Ben Boeckel
diff --git a/changelogs/unreleased/list_issues_with_no_labels.yml b/changelogs/unreleased/list_issues_with_no_labels.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ab44841631bafc975802fd2546d690ed51294596
--- /dev/null
+++ b/changelogs/unreleased/list_issues_with_no_labels.yml
@@ -0,0 +1,4 @@
+---
+title: Document ability to list issues with no labels using API
+merge_request: 9697
+author: Vignesh Ravichandran
diff --git a/changelogs/unreleased/long-file-name-overflow.yml b/changelogs/unreleased/long-file-name-overflow.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7ccf05491e176791d5ddfc20b0eb805ba571f038
--- /dev/null
+++ b/changelogs/unreleased/long-file-name-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed long file names overflowing under action buttons
+merge_request:
+author:
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b95e06954a69558978aff6593c8393e355b12ee
--- /dev/null
+++ b/changelogs/unreleased/make-karma-fast-again.yml
@@ -0,0 +1,4 @@
+---
+title: Only add code coverage instrumentation when generating coverage report
+merge_request: 9987
+author:
diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml
deleted file mode 100644
index c855f0cbcf7a62e139abf867780c1a2554277151..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/markdown-plantuml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: PlantUML support for Markdown
-merge_request: 8588
-author: Horacio Sanson
diff --git a/changelogs/unreleased/merge-request-tabs-fixture.yml b/changelogs/unreleased/merge-request-tabs-fixture.yml
deleted file mode 100644
index 289cd7b604a08edac1e64ffd0a15f4159d1ad301..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/merge-request-tabs-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for merge_request_tabs_spec.js
-merge_request: 9172
-author: winniehell
diff --git a/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce4d5092c17efb3efec3dca769d60c214e5416c3
--- /dev/null
+++ b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate SlackService and MattermostService from build_events to pipeline_events, and migrate BuildsEmailService to PipelinesEmailService. Update Hipchat to use pipeline events rather than build events.
+merge_request: 8196
+author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
deleted file mode 100644
index f32b3aea3c89e09f613511df9c7b81ef46e076d2..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: adds avatar for discussion note
-merge_request: 8734
-author:
diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24c6366177fc0ff97b706943e350061990b2c1a1
--- /dev/null
+++ b/changelogs/unreleased/mock-ci-service.yml
@@ -0,0 +1,4 @@
+---
+title: Add Mock CI service/integration for development
+merge_request:
+author:
diff --git a/changelogs/unreleased/moving-issue-with-two-list-labels.yml b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5ea81e3810a6b8f884d9976f9dd87ec20a4017d
--- /dev/null
+++ b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Removes label when moving issue to another list that it is currently in
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-diff-comment-button.yml b/changelogs/unreleased/mr-diff-comment-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1dc6ed1c495a09b5091efedf65e9e4c339aa9a58
--- /dev/null
+++ b/changelogs/unreleased/mr-diff-comment-button.yml
@@ -0,0 +1,4 @@
+---
+title: Improved diff comment button UX
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-tabs-container-offset.yml b/changelogs/unreleased/mr-tabs-container-offset.yml
deleted file mode 100644
index c5df8abfcf2a4bb209606e4cbbbb0834d737ae4a..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/mr-tabs-container-offset.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed merge requests tab extra margin when fixed to window
-merge_request:
-author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
deleted file mode 100644
index 5ce080b69126571942bee77932eb20268e91f5c6..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/newline-eslint-rule.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Flag multiple empty lines in eslint, fix offenses.
-merge_request: 8137
-author:
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
deleted file mode 100644
index 09e0b3a12d856a8c0d551d1c96d481040b3ac577..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: dismiss sidebar on repo buttons click
-merge_request: 8798
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml
deleted file mode 100644
index 6106c027360788635b60365aa6eafe76114888d5..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/no_project_notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support notes when a project is not specified (personal snippet notes)
-merge_request: 8468
-author:
diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e675ed945ad1bfde27ce0098c39d969291157f57
--- /dev/null
+++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
@@ -0,0 +1,4 @@
+---
+title: Only create unmergeable todos once when MR fails to merge
+merge_request:
+author:
diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
deleted file mode 100644
index c2e0410cc33b1326f37547c41f2a849343263cad..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to receive email notifications about your own activity
-merge_request: 8836
-author: Richard Macklin
diff --git a/changelogs/unreleased/pages-0-4-0.yml b/changelogs/unreleased/pages-0-4-0.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7286b25125e9359b808d58dff910febe5ad65e86
--- /dev/null
+++ b/changelogs/unreleased/pages-0-4-0.yml
@@ -0,0 +1,4 @@
+---
+title: Use GitLab Pages v0.4.0
+merge_request: 9896
+author:
diff --git a/changelogs/unreleased/pipeline-blocking-actions.yml b/changelogs/unreleased/pipeline-blocking-actions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6bde501de180aa11429ac11fcbe08c99b4f74b9e
--- /dev/null
+++ b/changelogs/unreleased/pipeline-blocking-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Make it possible to configure blocking manual actions
+merge_request: 9585
+author:
diff --git a/changelogs/unreleased/pipeline-tooltips-overflow.yml b/changelogs/unreleased/pipeline-tooltips-overflow.yml
new file mode 100644
index 0000000000000000000000000000000000000000..184da8049f3188215fd9e13a24fff8c6d51329ff
--- /dev/null
+++ b/changelogs/unreleased/pipeline-tooltips-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed pipeline actions tooltips overflowing
+merge_request:
+author:
diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml
deleted file mode 100644
index c2fa1a7fad0c5241d9c6746e870cb98a08649a2b..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/pms-lowercase-system-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make all system notes lowercase
-merge_request: 8807
-author:
diff --git a/changelogs/unreleased/priority-to-label-priority.yml b/changelogs/unreleased/priority-to-label-priority.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2d9c58bfd9b79a510923725441a13fdf770741bf
--- /dev/null
+++ b/changelogs/unreleased/priority-to-label-priority.yml
@@ -0,0 +1,4 @@
+---
+title: Rename priority sorting option to label priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
deleted file mode 100644
index 547a7c6755cfa1d998cb3c6c8ab1bcbe42cd3caf..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redesign searchbar in admin project list
-merge_request: 8776
-author:
diff --git a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
deleted file mode 100644
index e0f7e11b6d1aa1d9ebf0615d74b9c40437f9d092..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Search feature: redirects to commit page if query is commit sha and only commit
-  found'
-merge_request: 8028
-author: YarNayar
diff --git a/changelogs/unreleased/refresh-permissions-recent-users.yml b/changelogs/unreleased/refresh-permissions-recent-users.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4d08be6ed5c4ad1ee80d7a96fa0c4a5f3919555b
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-recent-users.yml
@@ -0,0 +1,4 @@
+---
+title: Reset users.authorized_projects_populated to automatically refresh user permissions
+merge_request:
+author:
diff --git a/changelogs/unreleased/relative-url-assets.yml b/changelogs/unreleased/relative-url-assets.yml
deleted file mode 100644
index 0877664aca4526c28f452d814ba28d3807f97d63..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/relative-url-assets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: allow relative url change without recompiling frontend assets
-merge_request: 8831
-author:
diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml
deleted file mode 100644
index 3ff69adb4d3ed7e327ebe8faa1d2d9c8c2ad726c..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/remove-deploy-key-endpoint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove /projects/:id/keys/.. endpoints'
-merge_request: 8716
-author: Robert Schilling
diff --git a/changelogs/unreleased/remove-es6-extension.yml b/changelogs/unreleased/remove-es6-extension.yml
new file mode 100644
index 0000000000000000000000000000000000000000..65f4a7a7867cafde26dd11ae0ca74a4443cc79fc
--- /dev/null
+++ b/changelogs/unreleased/remove-es6-extension.yml
@@ -0,0 +1,4 @@
+---
+title: Remove es6 file extension from JavaScript files
+merge_request: 9241
+author: winniehell
diff --git a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml
deleted file mode 100644
index b75b4644ba384e314322b1d5241bcdf1d4481e79..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove issue and MR counts from labels index
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c768f702ba29fdf664a53f45ac5bda498cd13dc4
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml
@@ -0,0 +1,4 @@
+---
+title: Removed jQuery UI highlight & autocomplete
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-new-relic-gem.yml b/changelogs/unreleased/remove-new-relic-gem.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b15ecd3e4e7c375d52643cab6b3d9198dda936bb
--- /dev/null
+++ b/changelogs/unreleased/remove-new-relic-gem.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the newrelic gem
+merge_request: 9622
+author: Robert Schilling
diff --git a/changelogs/unreleased/remove-readme-option.yml b/changelogs/unreleased/remove-readme-option.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1d4c862c00e14c199d4da587cf1e96b2630c26ad
--- /dev/null
+++ b/changelogs/unreleased/remove-readme-option.yml
@@ -0,0 +1,4 @@
+---
+title: Remove readme-only project view preference
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-subscribe-label-tooltip.yml b/changelogs/unreleased/remove-subscribe-label-tooltip.yml
new file mode 100644
index 0000000000000000000000000000000000000000..90b71d3be51e5d8c5fa560244d5290721a168dd1
--- /dev/null
+++ b/changelogs/unreleased/remove-subscribe-label-tooltip.yml
@@ -0,0 +1,4 @@
+---
+title: Remove tooltips from label subscription buttons
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-unused-ci-tables.yml b/changelogs/unreleased/remove-unused-ci-tables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fccfb882bd93455659eb6efdc5ae09c9f97fe13e
--- /dev/null
+++ b/changelogs/unreleased/remove-unused-ci-tables.yml
@@ -0,0 +1,4 @@
+---
+title: Remove various unused CI tables and columns
+merge_request: 9639
+author:
diff --git a/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4067b3de00c72d2a081e92cf852d17c7d5ecab19
--- /dev/null
+++ b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename table ci_commits to ci_pipelines
+merge_request: 9638
+author:
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d3109bdb17e38190b2919cdda94e00fef28e4066
--- /dev/null
+++ b/changelogs/unreleased/rename_all_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Rename 'All issues' to 'Open issues' in Add issues modal
+merge_request: 10042
+author: blackst0ne
diff --git a/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e799dd3b48d6887a4222a9714b2604082652ef1e
--- /dev/null
+++ b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
@@ -0,0 +1,4 @@
+---
+title: Change project count limit from 10 to 100000
+merge_request:
+author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
deleted file mode 100644
index 9b6df0c54af936f2addaf8dfb870fbe1c6405679..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/route-map.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add 'View on [env]' link to blobs and individual files in diffs
-merge_request: 8867
-author:
diff --git a/changelogs/unreleased/routes-lower-case.yml b/changelogs/unreleased/routes-lower-case.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2110956680c9c2f9e9ff4dff4a84d05e7d69bcda
--- /dev/null
+++ b/changelogs/unreleased/routes-lower-case.yml
@@ -0,0 +1,4 @@
+---
+title: Remove repeated routes.path check for postgresql database
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml
deleted file mode 100644
index c0c23fb6f112b1560c98a5b252f95572fa27bacc..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/rs-warden-blocked-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't perform Devise trackable updates on blocked User records
-merge_request: 8915
-author:
diff --git a/changelogs/unreleased/rss-btn-alignment-fix.yml b/changelogs/unreleased/rss-btn-alignment-fix.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c8f57ec0b7c2e21abf927223eb8e1f7272773b89
--- /dev/null
+++ b/changelogs/unreleased/rss-btn-alignment-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed RSS button alignment on activity pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/set-default-cache-key-for-jobs.yml b/changelogs/unreleased/set-default-cache-key-for-jobs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b69348d2ece4c42e5ca6153b7c09c84eb10f2c44
--- /dev/null
+++ b/changelogs/unreleased/set-default-cache-key-for-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Set default cache key to "default" for jobs
+merge_request: 9666
+author:
diff --git a/changelogs/unreleased/settings-tab.yml b/changelogs/unreleased/settings-tab.yml
new file mode 100644
index 0000000000000000000000000000000000000000..69990c9a917aa8d70ddfafa0008652c3811b699d
--- /dev/null
+++ b/changelogs/unreleased/settings-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved project settings from the gear drop-down menu to a tab
+merge_request: 9786
+author:
diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
deleted file mode 100644
index bab76812a173fb69a2e6d51ac887e5883c402a81..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add index to ci_trigger_requests for commit_id
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-add-labels-index.yml b/changelogs/unreleased/sh-add-labels-index.yml
deleted file mode 100644
index b948a75081c5acdaf99f90033c469629effb8bd1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/sh-add-labels-index.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add indices to improve loading of labels page
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
new file mode 100644
index 0000000000000000000000000000000000000000..57f1474093af45e83a5ae2afcfc992028de1621a
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
+merge_request:
+author:
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
new file mode 100644
index 0000000000000000000000000000000000000000..062626359ef2c65a87127a6a695ce63213744eb7
--- /dev/null
+++ b/changelogs/unreleased/simplify-docs-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify trigger_docs build job for CE and EE
+merge_request: 9820
+author: winniehell
diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml
deleted file mode 100644
index e6ffb94bd08127788be6ec75205d3ca1aeefe555..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/slash-commands-typo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed "substract" typo on /help/user/project/slash_commands
-merge_request: 8976
-author: Jason Aquino
diff --git a/changelogs/unreleased/small-screen-fullscreen-button.yml b/changelogs/unreleased/small-screen-fullscreen-button.yml
deleted file mode 100644
index f4c269bc473a71a525ad7235e577d0c5efa125b9..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/small-screen-fullscreen-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display fullscreen button on small screens
-merge_request: 5302
-author: winniehell
diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml
deleted file mode 100644
index 2895478abfd0d3059d1f658da72721ffe67dd054..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/snippets-search-performance.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduced query count for snippet search
-merge_request:
-author:
diff --git a/changelogs/unreleased/sort-builds-in-stage-dropdown.yml b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..646f25125b1f62baf0bbbeef2d2b8838990ffeee
--- /dev/null
+++ b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Sort builds in stage dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssh-key-paste.yml b/changelogs/unreleased/ssh-key-paste.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1e34ef60f6ee71f6560aaaf8338f6191b43aa3a0
--- /dev/null
+++ b/changelogs/unreleased/ssh-key-paste.yml
@@ -0,0 +1,4 @@
+---
+title: SSH key field updates title after pasting key
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssrf-protections.yml b/changelogs/unreleased/ssrf-protections.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8d8037380092630ca675d0bc53df5e7cb000bd27
--- /dev/null
+++ b/changelogs/unreleased/ssrf-protections.yml
@@ -0,0 +1,4 @@
+---
+title: To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-api-pipeline-jobs.yml b/changelogs/unreleased/tc-api-pipeline-jobs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..993c1b6526ac394233875505b41ef5de79721f0f
--- /dev/null
+++ b/changelogs/unreleased/tc-api-pipeline-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Add GET /projects/:id/pipelines/:pipeline_id/jobs endpoint
+merge_request: 9727
+author:
diff --git a/changelogs/unreleased/tc-fix-project-create-500.yml b/changelogs/unreleased/tc-fix-project-create-500.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1b746a41eabd514f377fe1897d89dbe463e64184
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-project-create-500.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for creating a project through API when import_url is nil
+merge_request: 9841
+author:
diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
deleted file mode 100644
index a7f5dcb560ce96aa2cfcea549d2606505b1104d9..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only show Merge Request button when user can create a MR
-merge_request: 8639
-author:
diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml
deleted file mode 100644
index db1e66770d1434dea0bed3ee04fb468e59aa0019..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/terminal-max-session-time.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce maximum session time for terminal websocket connection
-merge_request: 8413
-author:
diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a60b4d46a3f5d81b710f1d618ffa7ff6533aa22
--- /dev/null
+++ b/changelogs/unreleased/unified-member-api-response.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 400 for all validation erros in the mebers API'
+merge_request: 9523
+author: Robert Schilling
diff --git a/changelogs/unreleased/update-ace.yml b/changelogs/unreleased/update-ace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dbe476e3ae05df0970b37a943f8ddbe4b7958db4
--- /dev/null
+++ b/changelogs/unreleased/update-ace.yml
@@ -0,0 +1,4 @@
+---
+title: Update code editor (ACE) to 1.2.6, to fix input problems with compose key
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml
new file mode 100644
index 0000000000000000000000000000000000000000..acc42bf00b16966b9149276ff941eb1b330e80dd
--- /dev/null
+++ b/changelogs/unreleased/update-vue-2-1.yml
@@ -0,0 +1,4 @@
+---
+title: update Vue to v2.1.10
+merge_request: 9386
+author:
diff --git a/changelogs/unreleased/updated-pages-0-3-1.yml b/changelogs/unreleased/updated-pages-0-3-1.yml
deleted file mode 100644
index 8622b823c860969a10b26bc255e33a64ee6a00f9..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/updated-pages-0-3-1.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update GitLab Pages to v0.3.1
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml
deleted file mode 100644
index 55f9b3e407c48b0826009edfd3742de4c9a5f9aa..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/upgrade-babel-v6.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade babel 5.8.x to babel 6.22.x
-merge_request: 9072
-author:
diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml
deleted file mode 100644
index 7e0334566dcaa91dd1af307ec5b8f9dcff779b2d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/upgrade-omniauth.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Upgrade omniauth gem to 1.3.2
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml
deleted file mode 100644
index 6a49859d68cb446e99c29a967aef2e68bc3c45ce..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/upgrade-webpack-v2-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade to webpack v2.2
-merge_request: 9078
-author:
diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml
new file mode 100644
index 0000000000000000000000000000000000000000..381f80c5c0d3d0d46d632aa12214216167dd54e3
--- /dev/null
+++ b/changelogs/unreleased/use-corejs-polyfills.yml
@@ -0,0 +1,4 @@
+---
+title: Standardize on core-js for es2015 polyfills
+merge_request: 9749
+author:
diff --git a/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ff5a58f6232f0303fa529fc8742c76a79fff9f31
--- /dev/null
+++ b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml
@@ -0,0 +1,4 @@
+---
+title: Use redis channel to post notifications
+merge_request:
+author:
diff --git a/changelogs/unreleased/user-calendar-border.yml b/changelogs/unreleased/user-calendar-border.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8ebcca83256cfdaa67fb40e38593f320cbe0b064
--- /dev/null
+++ b/changelogs/unreleased/user-calendar-border.yml
@@ -0,0 +1,4 @@
+---
+title: Removed top border from user contribution calendar
+merge_request:
+author:
diff --git a/changelogs/unreleased/user-callouts.yml b/changelogs/unreleased/user-callouts.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f6ce06a3d8f58ee26a38961ef64e2a18e88c8d88
--- /dev/null
+++ b/changelogs/unreleased/user-callouts.yml
@@ -0,0 +1,4 @@
+---
+title: Added user callouts to the projects dashboard and user profile
+merge_request:
+author:
diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml
deleted file mode 100644
index 0083798be082cfe0141d1b291db2b89b8aac26f1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/wip-mr-from-commits.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Mark MR as WIP when pushing WIP commits
-merge_request: 8124
-author: Jurre Stender @jurre
diff --git a/changelogs/unreleased/workhorse-1-4-0.yml b/changelogs/unreleased/workhorse-1-4-0.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b55fabddb0fc7ad2077fcc5a727ce941ba296c44
--- /dev/null
+++ b/changelogs/unreleased/workhorse-1-4-0.yml
@@ -0,0 +1,4 @@
+---
+title: Use gitlab-workhorse 1.4.0
+merge_request: 9724
+author:
diff --git a/changelogs/unreleased/zj-builds-to-jobs-api.yml b/changelogs/unreleased/zj-builds-to-jobs-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..473dd9bc8edd98a83fd92d22b01f2e5043d987aa
--- /dev/null
+++ b/changelogs/unreleased/zj-builds-to-jobs-api.yml
@@ -0,0 +1,4 @@
+---
+title: Rename builds to job for the v4 API
+merge_request: 9463
+author:
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
deleted file mode 100644
index 2494884f5c92a5bad9b1f7b88f992dcf52194c04..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/zj-format-chat-messages.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reformat messages ChatOps
-merge_request: 8528
-author:
diff --git a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml b/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
deleted file mode 100644
index 044f4ae627d951d6e565673ea700d8714e130b05..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated GitlabCiService
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml
deleted file mode 100644
index 464c5948f8cd8d9f327d63fbf7a6936729aac6fd..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/zj-requeue-pending-delete.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Requeue pending deletion projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-variables-build-job.yml b/changelogs/unreleased/zj-variables-build-job.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1cb0919f8243a68724816e94e52652bec2b4d186
--- /dev/null
+++ b/changelogs/unreleased/zj-variables-build-job.yml
@@ -0,0 +1,4 @@
+---
+title: Rename job environment variables to new terminology
+merge_request: 9756
+author:
diff --git a/config/application.rb b/config/application.rb
index 9088d3c432b3fc8b9c01940a074a0db51606137d..f9f01b66473e48580f983f7bd3d5fcc0034c3f34 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -7,6 +7,7 @@ Bundler.require(:default, Rails.env)
 module Gitlab
   class Application < Rails::Application
     require_dependency Rails.root.join('lib/gitlab/redis')
+    require_dependency Rails.root.join('lib/gitlab/request_context')
 
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration should go into files in config/initializers
@@ -25,7 +26,8 @@ module Gitlab
                                      #{config.root}/app/models/hooks
                                      #{config.root}/app/models/members
                                      #{config.root}/app/models/project_services
-                                     #{config.root}/app/workers/concerns))
+                                     #{config.root}/app/workers/concerns
+                                     #{config.root}/app/services/concerns))
 
     config.generators.templates.push("#{config.root}/generator_templates")
 
@@ -90,6 +92,7 @@ module Gitlab
 
     # Enable the asset pipeline
     config.assets.enabled = true
+    # Support legacy unicode file named img emojis, `1F939.png`
     config.assets.paths << Gemojione.images_path
     config.assets.paths << "vendor/assets/fonts"
     config.assets.precompile << "*.png"
@@ -100,9 +103,6 @@ module Gitlab
     config.assets.precompile << "katex.js"
     config.assets.precompile << "xterm/xterm.css"
     config.assets.precompile << "lib/ace.js"
-    config.assets.precompile << "lib/cropper.js"
-    config.assets.precompile << "lib/raphael.js"
-    config.assets.precompile << "u2f.js"
     config.assets.precompile << "vendor/assets/fonts/*"
 
     # Version of your assets, change this if you want to expire all your assets
@@ -120,7 +120,7 @@ module Gitlab
           credentials: true,
           headers: :any,
           methods: :any,
-          expose: ['Link']
+          expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page']
       end
 
       # Cross-origin requests must not have the session cookie available
@@ -130,7 +130,7 @@ module Gitlab
           credentials: false,
           headers: :any,
           methods: :any,
-          expose: ['Link']
+          expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page']
       end
     end
 
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 7336d7c842abdee9ed1ceed6303411ec94f93119..072ed8a3864e360e2a0cec2a4aa2bc2feefbea0f 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -320,3 +320,9 @@
     :why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE
     :versions: []
     :when: 2017-02-08 22:35:00.225232000 Z
+- - :approve
+  - opener
+  - :who: Mike Greiling
+    :why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt
+    :versions: []
+    :when: 2017-02-21 22:33:41.729629000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a82ff605a70dc591f9e87b969129ff3de30e9906..ba7f6773985803fc9a87e565846cdaf6f8634517 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -89,7 +89,7 @@ production: &base
       issues: true
       merge_requests: true
       wiki: true
-      snippets: false
+      snippets: true
       builds: true
       container_registry: true
 
@@ -157,8 +157,8 @@ production: &base
     host: example.com
     port: 80 # Set to 443 if you serve the pages with HTTPS
     https: false # Set to true if you serve the pages with HTTPS
-    # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
-    # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+    # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
+    # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
 
   ## Mattermost
   ## For enabling Add to Mattermost button
@@ -177,9 +177,9 @@ production: &base
   # Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc.
   # Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
   cron_jobs:
-    # Flag stuck CI builds as failed
-    stuck_ci_builds_worker:
-      cron: "0 0 * * *"
+    # Flag stuck CI jobs as failed
+    stuck_ci_jobs_worker:
+      cron: "0 * * * *"
     # Remove expired build artifacts
     expire_build_artifacts_worker:
       cron: "50 * * * *"
@@ -441,19 +441,21 @@ production: &base
   shared:
     # path: /mnt/gitlab # Default: shared
 
+  # Gitaly settings
+  gitaly:
+    # The socket_path setting is optional and obsolete. When this is set
+    # GitLab assumes it can reach a Gitaly services via a Unix socket at
+    # this path. When this is commented out GitLab will not use Gitaly.
+    # 
+    # This setting is obsolete because we expect it to be moved under
+    # repositories/storages in GitLab 9.1.
+    #
+    # socket_path: tmp/sockets/gitaly.socket
 
   #
   # 4. Advanced settings
   # ==========================
 
-  # GitLab Satellites
-  #
-  # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
-  # least. This setting is fed to 'rm -rf' in
-  # db/migrate/20151023144219_remove_satellites.rb
-  satellites:
-    path: /home/git/gitlab-satellites/
-
   ## Repositories settings
   repositories:
     # Paths where repositories can be stored. Give the canonicalized absolute pathname.
@@ -461,7 +463,8 @@ production: &base
     # gitlab-shell invokes Dir.pwd inside the repository path and that results
     # real path not the symlink.
     storages: # You must have at least a `default` storage path.
-      default: /home/git/repositories/
+      default:
+        path: /home/git/repositories/
 
   ## Backup settings
   backup:
@@ -483,6 +486,8 @@ production: &base
     #   multipart_chunk_size: 104857600
     #   # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
     #   # encryption: 'AES256'
+    #   # Specifies Amazon S3 storage class to use for backups, this is optional
+    #   # storage_class: 'STANDARD'
 
   ## GitLab Shell settings
   gitlab_shell:
@@ -568,11 +573,10 @@ test:
     # In order to setup it correctly you need to specify
     # your system username you use to run GitLab
     # user: YOUR_USERNAME
-  satellites:
-    path: tmp/tests/gitlab-satellites/
   repositories:
     storages:
-      default: tmp/tests/repositories/
+      default:
+        path: tmp/tests/repositories/
   backup:
     path: tmp/tests/backups
   gitlab_shell:
@@ -586,7 +590,7 @@ test:
       new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new"
     jira:
       title: "JIRA"
-      url: https://sample_company.atlasian.net
+      url: https://sample_company.atlassian.net
       project_key: PROJECT
   ldap:
     enabled: false
diff --git a/config/initializers/inflections.rb b/config/initializers/0_inflections.rb
similarity index 100%
rename from config/initializers/inflections.rb
rename to config/initializers/0_inflections.rb
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 3f716dd8833dd4b21983b4709fd03bfb249be38c..62020fa9a75a3e32568eb03a0579cb8d7bba89d8 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -14,12 +14,15 @@ class Settings < Settingslogic
     end
 
     def build_gitlab_ci_url
-      if on_standard_port?(gitlab)
-        custom_port = nil
-      else
-        custom_port = ":#{gitlab.port}"
-      end
-      [ gitlab.protocol,
+      custom_port =
+        if on_standard_port?(gitlab)
+          nil
+        else
+          ":#{gitlab.port}"
+        end
+
+      [
+        gitlab.protocol,
         "://",
         gitlab.host,
         custom_port,
@@ -80,7 +83,9 @@ class Settings < Settingslogic
 
     def base_url(config)
       custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
-      [ config.protocol,
+
+      [
+        config.protocol,
         "://",
         config.host,
         custom_port
@@ -160,15 +165,16 @@ if github_settings
 
   github_settings["args"] ||= Settingslogic.new({})
 
-  if github_settings["url"].include?(github_default_url)
-    github_settings["args"]["client_options"] = OmniAuth::Strategies::GitHub.default_options[:client_options]
-  else
-    github_settings["args"]["client_options"] = {
-      "site"          => File.join(github_settings["url"], "api/v3"),
-      "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
-      "token_url"     => File.join(github_settings["url"], "login/oauth/access_token")
-    }
-  end
+  github_settings["args"]["client_options"] =
+    if github_settings["url"].include?(github_default_url)
+      OmniAuth::Strategies::GitHub.default_options[:client_options]
+    else
+      {
+        "site"          => File.join(github_settings["url"], "api/v3"),
+        "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
+        "token_url"     => File.join(github_settings["url"], "login/oauth/access_token")
+      }
+    end
 end
 
 Settings['shared'] ||= Settingslogic.new({})
@@ -180,7 +186,7 @@ Settings['issues_tracker'] ||= {}
 # GitLab
 #
 Settings['gitlab'] ||= Settingslogic.new({})
-Settings.gitlab['default_projects_limit'] ||= 10
+Settings.gitlab['default_projects_limit'] ||= 100000
 Settings.gitlab['default_branch_protection'] ||= 2
 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
 Settings.gitlab['host']       ||= ENV['GITLAB_HOST'] || 'localhost'
@@ -215,7 +221,7 @@ Settings.gitlab['session_expire_delay'] ||= 10080
 Settings.gitlab.default_projects_features['issues']             = true if Settings.gitlab.default_projects_features['issues'].nil?
 Settings.gitlab.default_projects_features['merge_requests']     = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
 Settings.gitlab.default_projects_features['wiki']               = true if Settings.gitlab.default_projects_features['wiki'].nil?
-Settings.gitlab.default_projects_features['snippets']           = false if Settings.gitlab.default_projects_features['snippets'].nil?
+Settings.gitlab.default_projects_features['snippets']           = true if Settings.gitlab.default_projects_features['snippets'].nil?
 Settings.gitlab.default_projects_features['builds']             = true if Settings.gitlab.default_projects_features['builds'].nil?
 Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
 Settings.gitlab.default_projects_features['visibility_level']   = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
@@ -272,8 +278,8 @@ Settings.pages['host']            ||= "example.com"
 Settings.pages['port']            ||= Settings.pages.https ? 443 : 80
 Settings.pages['protocol']        ||= Settings.pages.https ? "https" : "http"
 Settings.pages['url']             ||= Settings.send(:build_pages_url)
-Settings.pages['external_http']   ||= false if Settings.pages['external_http'].nil?
-Settings.pages['external_https']  ||= false if Settings.pages['external_https'].nil?
+Settings.pages['external_http']   ||= false unless Settings.pages['external_http'].present?
+Settings.pages['external_https']  ||= false unless Settings.pages['external_https'].present?
 
 #
 # Git LFS
@@ -302,9 +308,9 @@ Settings.gravatar['host']         = Settings.host_without_www(Settings.gravatar[
 # Cron Jobs
 #
 Settings['cron_jobs'] ||= Settingslogic.new({})
-Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
-Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
+Settings.cron_jobs['stuck_ci_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
 Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
 Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
@@ -360,8 +366,13 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s
 #
 Settings['repositories'] ||= Settingslogic.new({})
 Settings.repositories['storages'] ||= {}
-# Setting gitlab_shell.repos_path is DEPRECATED and WILL BE REMOVED in version 9.0
-Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/'
+unless Settings.repositories.storages['default']
+  Settings.repositories.storages['default'] ||= {}
+  # We set the path only if the default storage doesn't exist, in case it exists
+  # but follows the pre-9.0 configuration structure. `6_validations.rb` initializer
+  # will validate all storages and throw a relevant error to the user if necessary.
+  Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/'
+end
 
 #
 # The repository_downloads_path is used to remove outdated repository
@@ -370,11 +381,11 @@ Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'
 # data-integrity issue. In this case, we sets it to the default
 # repository_downloads_path value.
 #
-repositories_storages_path     = Settings.repositories.storages.values
+repositories_storages          = Settings.repositories.storages.values
 repository_downloads_path      = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '')
 repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
 
-if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) }
+if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) }
   Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
 end
 
@@ -393,6 +404,7 @@ if Settings.backup['upload']['connection']
 end
 Settings.backup['upload']['multipart_chunk_size'] ||= 104857600
 Settings.backup['upload']['encryption'] ||= nil
+Settings.backup['upload']['storage_class'] ||= nil
 
 #
 # Git
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92f64e164710d65e0a7574a90ed48a556c0caa3..9e24f42d28448b0947caf356f13187de655b0286 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -4,8 +4,8 @@ end
 
 def find_parent_path(name, path)
   parent = Pathname.new(path).realpath.parent
-  Gitlab.config.repositories.storages.detect do |n, p|
-    name != n && Pathname.new(p).realpath == parent
+  Gitlab.config.repositories.storages.detect do |n, rs|
+    name != n && Pathname.new(rs['path']).realpath == parent
   end
 end
 
@@ -13,17 +13,33 @@ def storage_validation_error(message)
   raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
 end
 
-def validate_storages
+def validate_storages_config
   storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
 
-  Gitlab.config.repositories.storages.each do |name, path|
+  Gitlab.config.repositories.storages.each do |name, repository_storage|
     storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
 
-    parent_name, _parent_path = find_parent_path(name, path)
+    if repository_storage.is_a?(String)
+      raise "#{name} is not a valid storage, because it has no `path` key. " \
+        "It may be configured as:\n\n#{name}:\n  path: #{repository_storage}\n\n" \
+        "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
+        "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
+    end
+
+    if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
+      storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
+    end
+  end
+end
+
+def validate_storages_paths
+  Gitlab.config.repositories.storages.each do |name, repository_storage|
+    parent_name, _parent_path = find_parent_path(name, repository_storage['path'])
     if parent_name
       storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
     end
   end
 end
 
-validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
+validate_storages_config
+validate_storages_paths unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
new file mode 100644
index 0000000000000000000000000000000000000000..07dd30f0a24897e8390672ac56cc369b3d377946
--- /dev/null
+++ b/config/initializers/8_gitaly.rb
@@ -0,0 +1,2 @@
+# Make sure we initialize a Gitaly channel before Sidekiq starts multi-threaded execution.
+Gitlab::GitalyClient.channel unless Rails.env.test?
diff --git a/config/initializers/metrics.rb b/config/initializers/8_metrics.rb
similarity index 88%
rename from config/initializers/metrics.rb
rename to config/initializers/8_metrics.rb
index e0702e06cc9f7d30264be919bd41133e9771f4e7..5e0eefdb154b3a05e9315a63821e5f2fad2dfa8e 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -20,13 +20,17 @@ def instrument_classes(instrumentation)
 
   # Path to search => prefix to strip from constant
   paths_to_instrument = {
-    ['app', 'finders']                    => ['app', 'finders'],
-    ['app', 'mailers', 'emails']          => ['app', 'mailers'],
-    ['app', 'services', '**']             => ['app', 'services'],
-    ['lib', 'gitlab', 'conflicts']        => ['lib'],
-    ['lib', 'gitlab', 'diff']             => ['lib'],
-    ['lib', 'gitlab', 'email', 'message'] => ['lib'],
-    ['lib', 'gitlab', 'checks']           => ['lib']
+    %w(app finders)                => %w(app finders),
+    %w(app mailers emails)         => %w(app mailers),
+    # Don't instrument `app/services/concerns`
+    # It contains modules that are included in the services.
+    # The services themselves are instrumented so the methods from the modules
+    # are included.
+    %w(app services [^concerns]**) => %w(app services),
+    %w(lib gitlab conflicts)       => ['lib'],
+    %w(lib gitlab diff)            => ['lib'],
+    %w(lib gitlab email message)   => ['lib'],
+    %w(lib gitlab checks)          => ['lib']
   }
 
   paths_to_instrument.each do |(path, prefix)|
@@ -120,9 +124,9 @@ if Gitlab::Metrics.enabled?
 
   # These are manually require'd so the classes are registered properly with
   # ActiveSupport.
-  require 'gitlab/metrics/subscribers/action_view'
-  require 'gitlab/metrics/subscribers/active_record'
-  require 'gitlab/metrics/subscribers/rails_cache'
+  require_dependency 'gitlab/metrics/subscribers/action_view'
+  require_dependency 'gitlab/metrics/subscribers/active_record'
+  require_dependency 'gitlab/metrics/subscribers/rails_cache'
 
   Gitlab::Application.configure do |config|
     config.middleware.use(Gitlab::Metrics::RackMiddleware)
diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/acts_as_taggable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c564c0cab11342ae8f7f2977cedd13c44667b73d
--- /dev/null
+++ b/config/initializers/acts_as_taggable.rb
@@ -0,0 +1,5 @@
+ActsAsTaggableOn.strict_case_match = true
+
+# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed
+# since the count is not used anywhere its better performance wise to disable this cache
+ActsAsTaggableOn.tags_counter = false
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 738dbeefc118142ee97b6f34b83977886d62efc0..3b1317030bcc7ae873809fb68af6ba7f0ce15c8b 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -24,7 +24,7 @@ Devise.setup do |config|
   # session. If you need permissions, you should implement that in a before filter.
   # You can also supply a hash where the value is a boolean determining whether
   # or not authentication should be aborted when the value is not present.
-  config.authentication_keys = [ :login ]
+  config.authentication_keys = [:login]
 
   # Configure parameters from the request object used for authentication. Each entry
   # given should be a request method and it will automatically be passed to the
@@ -36,12 +36,12 @@ Devise.setup do |config|
   # Configure which authentication keys should be case-insensitive.
   # These keys will be downcased upon creating or modifying a user and when used
   # to authenticate or find a user. Default is :email.
-  config.case_insensitive_keys = [ :email ]
+  config.case_insensitive_keys = [:email]
 
   # Configure which authentication keys should have whitespace stripped.
   # These keys will have whitespace before and after removed upon creating or
   # modifying a user and when used to authenticate or find a user. Default is :email.
-  config.strip_whitespace_keys = [ :email ]
+  config.strip_whitespace_keys = [:email]
 
   # Tell if authentication through request.params is enabled. True by default.
   # config.params_authenticatable = true
@@ -124,7 +124,7 @@ Devise.setup do |config|
   config.lock_strategy = :failed_attempts
 
   # Defines which key will be used when locking and unlocking an account
-  config.unlock_keys = [ :email ]
+  config.unlock_keys = [:email]
 
   # Defines which strategy will be used to unlock an account.
   # :email = Sends an unlock link to the user email
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 88cd0f5f6524dffc586a6e04ac5ad2907e7f6463..a56367657746cb1c31ee6a979f4dcfe16c8774c5 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -6,9 +6,14 @@ Doorkeeper.configure do
   # This block will be called to check whether the resource owner is authenticated or not.
   resource_owner_authenticator do
     # Put your resource owner authentication logic here.
-    # Ensure user is redirected to redirect_uri after login
-    session[:user_return_to] = request.fullpath
-    current_user || redirect_to(new_user_session_url)
+    if current_user
+      current_user
+    else
+      # Ensure user is redirected to redirect_uri after login
+      session[:user_return_to] = request.fullpath
+      redirect_to(new_user_session_url)
+      nil
+    end
   end
 
   resource_owner_from_credentials do |routes|
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
new file mode 100644
index 0000000000000000000000000000000000000000..700ca25b88494bd97e515d5a5364fb0591bfedf1
--- /dev/null
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -0,0 +1,36 @@
+Doorkeeper::OpenidConnect.configure do
+  issuer Gitlab.config.gitlab.url
+
+  jws_private_key Rails.application.secrets.jws_private_key
+
+  resource_owner_from_access_token do |access_token|
+    User.active.find_by(id: access_token.resource_owner_id)
+  end
+
+  auth_time_from_resource_owner do |user|
+    user.current_sign_in_at
+  end
+
+  reauthenticate_resource_owner do |user, return_to|
+    store_location_for user, return_to
+    sign_out user
+    redirect_to new_user_session_url
+  end
+
+  subject do |user|
+    # hash the user's ID with the Rails secret_key_base to avoid revealing it
+    Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
+  end
+
+  claims do
+    with_options scope: :openid do |o|
+      o.claim(:name)           { |user| user.name }
+      o.claim(:nickname)       { |user| user.username }
+      o.claim(:email)          { |user| user.public_email  }
+      o.claim(:email_verified) { |user| true if user.public_email? }
+      o.claim(:website)        { |user| user.full_website_url if user.website_url? }
+      o.claim(:profile)        { |user| Rails.application.routes.url_helpers.user_url user }
+      o.claim(:picture)        { |user| user.avatar_url }
+    end
+  end
+end
diff --git a/config/initializers/etag_caching.rb b/config/initializers/etag_caching.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eba888011418c5789c1f1a740c1b4b10d660407e
--- /dev/null
+++ b/config/initializers/etag_caching.rb
@@ -0,0 +1,4 @@
+# This middleware has to come after Gitlab::Metrics::RackMiddleware
+# in the middleware stack, because it tracks events with
+# GitLab Performance Monitoring
+Rails.application.config.middleware.use(Gitlab::EtagCaching::Middleware)
diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb37f9ed22c3ac4b4b05867f1514f3fd141ca148
--- /dev/null
+++ b/config/initializers/fix_local_cache_middleware.rb
@@ -0,0 +1,24 @@
+module LocalCacheRegistryCleanupWithEnsure
+  LocalCacheRegistry =
+    ActiveSupport::Cache::Strategy::LocalCache::LocalCacheRegistry
+  LocalStore =
+    ActiveSupport::Cache::Strategy::LocalCache::LocalStore
+
+  def call(env)
+    LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+    response = @app.call(env)
+    response[2] = ::Rack::BodyProxy.new(response[2]) do
+      LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+    end
+    cleanup_after_response = true # ADDED THIS LINE
+    response
+  rescue Rack::Utils::InvalidParameterError
+    [400, {}, []]
+  ensure # ADDED ensure CLAUSE to cleanup when something is thrown
+    LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
+      cleanup_after_response
+  end
+end
+
+ActiveSupport::Cache::Strategy::LocalCache::Middleware
+  .prepend(LocalCacheRegistryCleanupWithEnsure)
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 703f24f93b27e4fae2dfc99379e1bb2d34396d46..1ebe3c7a74230aae9031472c4581029aae129aca 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -1,5 +1,5 @@
 module Gollum
-  GIT_ADAPTER = "rugged"
+  GIT_ADAPTER = "rugged".freeze
 end
 require "gollum-lib"
 
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 4c91a61fb4a77ef627873749d912d516410dc947..959daa93f78c629bf7c93749c824eec0ca0eb325 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -1,4 +1,4 @@
 HealthCheck.setup do |config|
-  config.standard_checks = ['database', 'migrations', 'cache']
-  config.full_checks = ['database', 'migrations', 'cache']
+  config.standard_checks = %w(database migrations cache)
+  config.full_checks = %w(database migrations cache)
 end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index ab5a0561b8c6d71a2334ba121bc8e782587f6677..f7fa6d1c2de38bb41af366eee0877c9711810ba8 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -20,15 +20,12 @@ OmniAuth.config.before_request_phase do |env|
 end
 
 if Gitlab.config.omniauth.enabled
-  Gitlab.config.omniauth.providers.each do |provider|
-    if provider['name'] == 'kerberos'
-      require 'omniauth-kerberos'
-    end
-  end
+  provider_names = Gitlab.config.omniauth.providers.map(&:name)
+  require 'omniauth-kerberos' if provider_names.include?('kerberos')
 end
 
 module OmniAuth
   module Strategies
-    autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+    autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
   end
 end
diff --git a/config/initializers/request_context.rb b/config/initializers/request_context.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0b485fc1adc5e411efacb01d830869527cbce7a0
--- /dev/null
+++ b/config/initializers/request_context.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do |config|
+  config.middleware.insert_after RequestStore::Middleware, Gitlab::RequestContext
+end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 0ef9f51e5cf06e9117ef0a4a4dc66d600f6afa3a..70177995356d05a5ba60da7e7a450d4d116d5cd3 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -1,22 +1,41 @@
-module RspecProfilingConnection
-  def establish_connection
-    ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+module RspecProfilingExt
+  module PSQL
+    def establish_connection
+      ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+    end
   end
-end
 
-module RspecProfilingGitBranchCi
-  def branch
-    ENV['CI_BUILD_REF_NAME'] || super
+  module Git
+    def branch
+      ENV['CI_COMMIT_REF_NAME'] || super
+    end
+  end
+
+  module Run
+    def example_finished(*args)
+      super
+    rescue => err
+      return if @already_logged_example_finished_error
+
+      $stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed."
+      @already_logged_example_finished_error = true
+    end
+
+    alias_method :example_passed, :example_finished
+    alias_method :example_failed, :example_finished
   end
 end
 
 if Rails.env.test?
   RspecProfiling.configure do |config|
     if ENV['RSPEC_PROFILING_POSTGRES_URL']
-      RspecProfiling::Collectors::PSQL.prepend(RspecProfilingConnection)
+      RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
       config.collector = RspecProfiling::Collectors::PSQL
     end
   end
 
-  RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI')
+  if ENV.has_key?('CI')
+    RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
+    RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+  end
 end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 291fa6c0abcfd0f3361d585642c2803373aa3f34..f9c1d2165d3d9c1436c1b703cccba35c0f23b9f0 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -24,7 +24,8 @@ def create_tokens
   defaults = {
     secret_key_base: file_secret_key || generate_new_secure_token,
     otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
-    db_key_base: generate_new_secure_token
+    db_key_base: generate_new_secure_token,
+    jws_private_key: generate_new_rsa_private_key
   }
 
   missing_secrets = set_missing_keys(defaults)
@@ -41,6 +42,10 @@ def generate_new_secure_token
   SecureRandom.hex(64)
 end
 
+def generate_new_rsa_private_key
+  OpenSSL::PKey::RSA.new(2048).to_pem
+end
+
 def warn_missing_secret(secret)
   warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
 end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0c4516b70f03769d61197cf26c20bba5278178d8..ecd7395648837341a6ce33c523f8fb9619fdaac4 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -19,6 +19,12 @@ Sidekiq.configure_server do |config|
     chain.add Gitlab::SidekiqStatus::ClientMiddleware
   end
 
+  config.on :startup do
+    # Clear any connections that might have been obtained before starting
+    # Sidekiq (e.g. in an initializer).
+    ActiveRecord::Base.clear_all_connections!
+  end
+
   # Sidekiq-cron: load recurring jobs from gitlab.yml
   # UGLY Hack to get nested hash from settingslogic
   cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
@@ -36,7 +42,7 @@ Sidekiq.configure_server do |config|
 
   Gitlab::SidekiqThrottler.execute!
 
-  config = ActiveRecord::Base.configurations[Rails.env] ||
+  config = Gitlab::Database.config ||
     Rails.application.config.database_configuration[Rails.env]
   config['pool'] = Sidekiq.options[:concurrency]
   ActiveRecord::Base.establish_connection(config)
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index cd869657c530f7f0b6c1b76e45d25a3d0fedd973..fc4f02453d77daa25c77586fead3db1bb239bb4d 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -21,4 +21,4 @@ gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |pro
 end.compact
 
 Rails.application.config.action_dispatch.trusted_proxies = (
-  [ '127.0.0.1', '::1' ] + gitlab_trusted_proxies)
+  ['127.0.0.1', '::1'] + gitlab_trusted_proxies)
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d83fb92d5609bc234ed17f9d9333a5fb9a0e550
--- /dev/null
+++ b/config/initializers/warden.rb
@@ -0,0 +1,5 @@
+Rails.application.configure do |config|
+  Warden::Manager.after_set_user do |user, auth, opts|
+    Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
+  end
+end
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
index 84d809741c47d1c88a7afdf0c6f4d239b72933c6..064e5964f097bb0e5132caa3a16573acce28e0f6 100644
--- a/config/initializers/workhorse_multipart.rb
+++ b/config/initializers/workhorse_multipart.rb
@@ -10,7 +10,7 @@ end
 #
 module Gitlab
   module StrongParameterScalars
-    GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
+    GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile].freeze
 
     def permitted_scalar?(value)
       super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
diff --git a/config/karma.config.js b/config/karma.config.js
index 2f3cc9324131acde61cc0d28e1e3b8d0c2074102..eb082dd28bfdff962770e4a7101ce5a986b41c24 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,22 +1,23 @@
 var path = require('path');
+var webpack = require('webpack');
 var webpackConfig = require('./webpack.config.js');
 var ROOT_PATH = path.resolve(__dirname, '..');
 
-// add coverage instrumentation to babel config
-if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) {
-  var babelConfig = webpackConfig.module.rules.find(function (rule) {
-    return rule.loader === 'babel-loader';
+// remove problematic plugins
+if (webpackConfig.plugins) {
+  webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
+    return !(
+      plugin instanceof webpack.optimize.CommonsChunkPlugin ||
+      plugin instanceof webpack.DefinePlugin
+    );
   });
-
-  babelConfig.options = babelConfig.options || {};
-  babelConfig.options.plugins = babelConfig.options.plugins || [];
-  babelConfig.options.plugins.push('istanbul');
 }
 
 // Karma configuration
 module.exports = function(config) {
   var progressReporter = process.env.CI ? 'mocha' : 'progress';
-  config.set({
+
+  var karmaConfig = {
     basePath: ROOT_PATH,
     browsers: ['PhantomJS'],
     frameworks: ['jasmine'],
@@ -25,16 +26,22 @@ module.exports = function(config) {
       { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
     ],
     preprocessors: {
-      'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+      'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
     },
-    reporters: [progressReporter, 'coverage-istanbul'],
-    coverageIstanbulReporter: {
+    reporters: [progressReporter],
+    webpack: webpackConfig,
+    webpackMiddleware: { stats: 'errors-only' },
+  };
+
+  if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
+    karmaConfig.reporters.push('coverage-istanbul');
+    karmaConfig.coverageIstanbulReporter = {
       reports: ['html', 'text-summary'],
       dir: 'coverage-javascript/',
       subdir: '.',
       fixWebpackSourcePaths: true
-    },
-    webpack: webpackConfig,
-    webpackMiddleware: { stats: 'errors-only' },
-  });
+    };
+  }
+
+  config.set(karmaConfig);
 };
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 1d728282d90fcbf17d20ca88870a6a6903a55d99..14d49885fb3bdc8b7c92d293069d0a5a6327bb19 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -60,6 +60,7 @@ en:
     scopes:
       api: Access your API
       read_user: Read user information
+      openid: Authenticate using OpenID Connect
 
     flash:
       applications:
diff --git a/config/newrelic.yml b/config/newrelic.yml
deleted file mode 100644
index 9ef922a38d9c969e4a98b551e31a785f32d25d7b..0000000000000000000000000000000000000000
--- a/config/newrelic.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-# New Relic configuration file
-#
-# This file is here to make sure the New Relic gem stays
-# quiet by default.
-#
-# To enable and configure New Relic, please use
-# environment variables, e.g. NEW_RELIC_ENABLED=true
-
-production:
-  enabled: false
-
-development:
-  enabled: false
-
-test:
-  enabled: false
diff --git a/config/routes.rb b/config/routes.rb
index 06d565df469ca28351e98e3318ac6c365daa8813..1a851da6203195f2f07af8899f027cc0ee3de58c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,14 +22,13 @@ Rails.application.routes.draw do
                 authorizations: 'oauth/authorizations'
   end
 
+  use_doorkeeper_openid_connect
+
   # Autocomplete
   get '/autocomplete/users' => 'autocomplete#users'
   get '/autocomplete/users/:id' => 'autocomplete#user'
   get '/autocomplete/projects' => 'autocomplete#projects'
 
-  # Emojis
-  resources :emojis, only: :index
-
   # Search
   get 'search' => 'search#show'
   get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index b09c05826a7279e248e9c06c2245062f261581ae..fcbe2e2c43586021f426e4756168c25fa4c2cb19 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -2,6 +2,11 @@ namespace :admin do
   resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
     resources :keys, only: [:show, :destroy]
     resources :identities, except: [:show]
+    resources :impersonation_tokens, only: [:index, :create] do
+      member do
+        put :revoke
+      end
+    end
 
     member do
       get :projects
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index 47a049d5b204381e417668808aebb9c79b201703..8d23aa8fbf62a982ad5f84bd5b6cc82a7ebc17b0 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -5,11 +5,5 @@ namespace :ci do
 
   resource :lint, only: [:show, :create]
 
-  resources :projects, only: [:index, :show] do
-    member do
-      get :status, to: 'projects#badge'
-    end
-  end
-
-  root to: 'projects#index'
+  root to: redirect('/')
 end
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index adc3ad207cc79ef92a0c6d76421dba73d6086b9e..8e380a0b0ace12c23442e4422ba486430157a68e 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do
     resources :todos, only: [:index, :destroy] do
       collection do
         delete :destroy_all
+        patch :bulk_restore
       end
       member do
         patch :restore
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 6b91485da9e6a2d1c25afe2e8736e6daa51797aa..07c341999eac83d90b0ac87ff4d0b20094683595 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -21,7 +21,7 @@ resource :profile, only: [:show, :update] do
       end
     end
     resource :preferences, only: [:show, :update]
-    resources :keys, only: [:index, :show, :new, :create, :destroy]
+    resources :keys, only: [:index, :show, :create, :destroy]
     resources :emails, only: [:index, :create, :destroy]
     resources :chat_names, only: [:index, :new, :create, :destroy] do
       collection do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 84f123ff7172c66e9bd1b0b71e4f3cfdfa4005de..44b8ae7aeddffa91bdb30a9ccbc3141fe0b9da3e 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do
 
       resources :autocomplete_sources, only: [] do
         collection do
-          get 'emojis'
           get 'members'
           get 'issues'
           get 'merge_requests'
@@ -58,6 +57,7 @@ constraints(ProjectUrlConstrainer.new) do
 
         resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
           member do
+            get :charts
             get :commits
             get :ci
             get :languages
@@ -100,7 +100,7 @@ constraints(ProjectUrlConstrainer.new) do
           get :merge_check
           post :merge
           get :merge_widget_refresh
-          post :cancel_merge_when_build_succeeds
+          post :cancel_merge_when_pipeline_succeeds
           get :ci_status
           get :ci_environments_status
           post :toggle_subscription
@@ -135,11 +135,16 @@ constraints(ProjectUrlConstrainer.new) do
 
       resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
       resources :variables, only: [:index, :show, :update, :create, :destroy]
-      resources :triggers, only: [:index, :create, :destroy]
+      resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
+        member do
+          post :take_ownership
+        end
+      end
 
       resources :pipelines, only: [:index, :new, :create, :show] do
         collection do
           resource :pipelines_settings, path: 'settings', only: [:show, :update]
+          get :charts
         end
 
         member do
@@ -154,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do
         member do
           post :stop
           get :terminal
+          get :metrics
           get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
         end
 
@@ -265,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do
 
       resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
 
-      resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+      resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
         member do
           delete :delete_attachment
           post :resolve
@@ -273,6 +279,8 @@ constraints(ProjectUrlConstrainer.new) do
         end
       end
 
+      get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
+
       resources :boards, only: [:index, :show] do
         scope module: :boards do
           resources :issues, only: [:index, :update]
@@ -321,6 +329,7 @@ constraints(ProjectUrlConstrainer.new) do
         resource :members, only: [:show]
         resource :ci_cd, only: [:show], controller: 'ci_cd'
         resource :integrations, only: [:show]
+        resource :repository, only: [:show], controller: :repository
       end
 
       # Since both wiki and repository routing contains wildcard characters
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index dad746d59a15fb145958e5ffb016beaa30709e98..a6b3f5d469362107604ba11916d9e7646687ae5c 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -1,4 +1,4 @@
-WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
 
 scope(controller: :wikis) do
   scope(path: 'wikis', as: :wikis) do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 97620cc9c7f2c91e558f06a06bd719c46e836e86..9d2066a649021ef84c184bf646a8ffdda9c43c22 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,7 @@
   - [email_receiver, 2]
   - [emails_on_push, 2]
   - [mailers, 2]
+  - [upload_checksum, 1]
   - [use_key, 1]
   - [repository_fork, 1]
   - [repository_import, 1]
@@ -51,3 +52,4 @@
   - [cronjob, 1]
   - [default, 1]
   - [pages, 1]
+  - [system_hook_push, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 158999938749e02451b53d18cde0cbfdf040b326..c6794d6b944f58589c80ea249fa8753309183c36 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -5,17 +5,22 @@ var path = require('path');
 var webpack = require('webpack');
 var StatsPlugin = require('stats-webpack-plugin');
 var CompressionPlugin = require('compression-webpack-plugin');
+var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 var ROOT_PATH = path.resolve(__dirname, '..');
 var IS_PRODUCTION = process.env.NODE_ENV === 'production';
 var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
 var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
 var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
 
 var config = {
   context: path.join(ROOT_PATH, 'app/assets/javascripts'),
   entry: {
-    application:          './application.js',
+    common:               './commons/index.js',
+    common_vue:           ['vue', 'vue-resource'],
+    common_d3:            ['d3'],
+    main:                 './main.js',
     blob_edit:            './blob_edit/blob_edit_bundle.js',
     boards:               './boards/boards_bundle.js',
     simulate_drag:        './test_utils/simulate_drag.js',
@@ -26,25 +31,25 @@ var config = {
     environments_folder:  './environments/folder/environments_folder_bundle.js',
     filtered_search:      './filtered_search/filtered_search_bundle.js',
     graphs:               './graphs/graphs_bundle.js',
+    groups_list:          './groups_list.js',
     issuable:             './issuable/issuable_bundle.js',
     merge_conflicts:      './merge_conflicts/merge_conflicts_bundle.js',
     merge_request_widget: './merge_request_widget/ci_bundle.js',
+    monitoring:           './monitoring/monitoring_bundle.js',
     network:              './network/network_bundle.js',
     profile:              './profile/profile_bundle.js',
     protected_branches:   './protected_branches/protected_branches_bundle.js',
     snippet:              './snippet/snippet_bundle.js',
     terminal:             './terminal/terminal_bundle.js',
+    u2f:                  ['vendor/u2f'],
     users:                './users/users_bundle.js',
-    lib_chart:            './lib/chart.js',
-    lib_d3:               './lib/d3.js',
-    lib_vue:              './lib/vue_resource.js',
     vue_pipelines:        './vue_pipelines_index/index.js',
   },
 
   output: {
     path: path.join(ROOT_PATH, 'public/assets/webpack'),
     publicPath: '/assets/webpack/',
-    filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js'
+    filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
   },
 
   devtool: 'inline-source-map',
@@ -52,15 +57,13 @@ var config = {
   module: {
     rules: [
       {
-        test: /\.(js|es6)$/,
+        test: /\.js$/,
         exclude: /(node_modules|vendor\/assets)/,
-        loader: 'babel-loader',
-        options: {
-          presets: [
-            ["es2015", {"modules": false}],
-            'stage-2'
-          ]
-        }
+        loader: 'babel-loader'
+      },
+      {
+        test: /\.svg$/,
+        use: 'raw-loader'
       }
     ]
   },
@@ -75,17 +78,61 @@ var config = {
       modules: false,
       assets: true
     }),
+
+    // prevent pikaday from including moment.js
     new webpack.IgnorePlugin(/moment/, /pikaday/),
+
+    // fix legacy jQuery plugins which depend on globals
+    new webpack.ProvidePlugin({
+      $: 'jquery',
+      jQuery: 'jquery',
+    }),
+
+    // use deterministic module ids in all environments
+    IS_PRODUCTION ?
+      new webpack.HashedModuleIdsPlugin() :
+      new webpack.NamedModulesPlugin(),
+
+    // create cacheable common library bundle for all vue chunks
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common_vue',
+      chunks: [
+        'boards',
+        'commit_pipelines',
+        'cycle_analytics',
+        'diff_notes',
+        'environments',
+        'environments_folder',
+        'issuable',
+        'merge_conflicts',
+        'vue_pipelines',
+      ],
+      minChunks: function(module, count) {
+        return module.resource && (/vue_shared/).test(module.resource);
+      },
+    }),
+
+    // create cacheable common library bundle for all d3 chunks
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common_d3',
+      chunks: ['graphs', 'users', 'monitoring'],
+    }),
+
+    // create cacheable common library bundles
+    new webpack.optimize.CommonsChunkPlugin({
+      names: ['main', 'common', 'runtime'],
+    }),
   ],
 
   resolve: {
-    extensions: ['.js', '.es6', '.js.es6'],
+    extensions: ['.js'],
     alias: {
       '~':              path.join(ROOT_PATH, 'app/assets/javascripts'),
-      'bootstrap/js':   'bootstrap-sass/assets/javascripts/bootstrap',
-      'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
+      'emojis':         path.join(ROOT_PATH, 'fixtures/emojis'),
+      'empty_states':   path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+      'icons':          path.join(ROOT_PATH, 'app/views/shared/icons'),
       'vendor':         path.join(ROOT_PATH, 'vendor/assets/javascripts'),
-      'vue$':           IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
+      'vue$':           'vue/dist/vue.common.js',
     }
   }
 }
@@ -120,4 +167,16 @@ if (IS_DEV_SERVER) {
   config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
 }
 
+if (WEBPACK_REPORT) {
+  config.plugins.push(
+    new BundleAnalyzerPlugin({
+      analyzerMode: 'static',
+      generateStatsFile: true,
+      openAnalyzer: false,
+      reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
+      statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
+    })
+  );
+}
+
 module.exports = config;
diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb
index 29b8081055d30a429f09680d4e154277157b2760..bc2d74c8034aac8710d422ee44c4ec193f7b6134 100644
--- a/db/fixtures/development/13_comments.rb
+++ b/db/fixtures/development/13_comments.rb
@@ -1,7 +1,7 @@
 require './spec/support/sidekiq'
 
 Gitlab::Seeder.quiet do
-  Issue.all.each do |issue|
+  Issue.find_each do |issue|
     project = issue.project
 
     project.team.users.each do |user|
@@ -16,7 +16,7 @@ Gitlab::Seeder.quiet do
     end
   end
 
-  MergeRequest.all.each do |mr|
+  MergeRequest.find_each do |mr|
     project = mr.project
 
     project.team.users.each do |user|
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
index ea343c26b69a3e3ff847f9409d85c6290b558bfb..137a036edaf057f958d8513c542077e6ec3445fb 100644
--- a/db/fixtures/development/15_award_emoji.rb
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -1,7 +1,7 @@
 require './spec/support/sidekiq'
 
 Gitlab::Seeder.quiet do
-  emoji = Gitlab::AwardEmoji.emojis.keys
+  emoji = Gitlab::Emoji.emojis.keys
 
   Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
     project = issue.project
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 747901dd63488e622eae4e07dc7fd43d70200696..4bc735916c18c9ed9ae83ba6cd731fd380da47b5 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics
 
       issue.project.repository.add_branch(@user, branch_name, 'master')
 
-      options = {
-        committer: issue.project.repository.user_to_committer(@user),
-        author: issue.project.repository.user_to_committer(@user),
-        commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
-        file: { content: "content", path: filename, update: false }
-      }
-
-      commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+      commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
       issue.project.repository.commit(commit_sha)
 
-
       GitPushService.new(issue.project,
                          @user,
                          oldrev: issue.project.repository.commit("master").sha,
diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/19_nested_groups.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d8dddc3fee96cdeb6fd709c9d169b4582db6413c
--- /dev/null
+++ b/db/fixtures/development/19_nested_groups.rb
@@ -0,0 +1,69 @@
+require './spec/support/sidekiq'
+
+def create_group_with_parents(user, full_path)
+  parent_path = nil
+  group = nil
+
+  until full_path.blank?
+    path, _, full_path = full_path.partition('/')
+
+    if parent_path
+      parent = Group.find_by_full_path(parent_path)
+
+      parent_path += '/'
+      parent_path += path
+
+      group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute
+    else
+      parent_path = path
+
+      group = Group.find_by_full_path(parent_path) ||
+        Groups::CreateService.new(user, path: path).execute
+    end
+  end
+
+  group
+end
+
+Sidekiq::Testing.inline! do
+  Gitlab::Seeder.quiet do
+    project_urls = [
+     'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
+     'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
+     'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
+     'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
+     'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
+     'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
+     'https://android.googlesource.com/platform/hardware/bsp/intel.git',
+     'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
+     'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
+    ]
+
+    user = User.admins.first
+
+    project_urls.each_with_index do |url, i|
+      full_path = url.sub('https://android.googlesource.com/', '')
+      full_path = full_path.sub(/\.git\z/, '')
+      full_path, _, project_path = full_path.rpartition('/')
+      group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
+
+      params = {
+        import_url: url,
+        namespace_id: group.id,
+        path: project_path,
+        name: project_path,
+        description: FFaker::Lorem.sentence,
+        visibility_level: Gitlab::VisibilityLevel.values.sample
+      }
+
+      project = Projects::CreateService.new(user, params).execute
+      project.send(:_run_after_commit_queue)
+
+      if project.valid?
+        print '.'
+      else
+        print 'F'
+      end
+    end
+  end
+end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index e8de7ccf3db39dce3575d533a7fc1cf600510ae4..66203486d532099dfbd10d2d81243e012e42060d 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -8,7 +8,7 @@ class MigrateRepoSize < ActiveRecord::Migration
     project_data.each do |project|
       id = project['id']
       namespace_path = project['namespace_path'] || ''
-      repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+      repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default['path']
       path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
 
       begin
diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb
index 760b766828e90420ccf6c38cbe5774bd115af5ac..cd8b505de9f94e9c25ac9996e32e961b19b13379 100644
--- a/db/migrate/20160610201627_migrate_users_notification_level.rb
+++ b/db/migrate/20160610201627_migrate_users_notification_level.rb
@@ -1,7 +1,11 @@
 class MigrateUsersNotificationLevel < ActiveRecord::Migration
+  DOWNTIME = false
+
   # Migrates only users who changed their default notification level :participating
   # creating a new record on notification settings table
 
+  DOWNTIME = false
+
   def up
     execute(%Q{
       INSERT INTO notification_settings
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
index 63f7392e54fc073445b86680931dc61c21e1fa39..7a8ed99c68f01cd1b0ac34179ad0990d847319cb 100644
--- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -1,9 +1,15 @@
 class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
 
+  DOWNTIME = false
+
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :members, :requested_at
   end
+
+  def down
+    remove_index :members, :requested_at if index_exists? :members, :requested_at
+  end
 end
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
index dfa5110dea4b5ce80075c8c59573311a47f07144..6ca486c63d1eac8dea575b17cd3dd2d89b995a0a 100644
--- a/db/migrate/20160620115026_add_index_on_runners_locked.rb
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -4,9 +4,15 @@
 class AddIndexOnRunnersLocked < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
 
+  DOWNTIME = false
+
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :ci_runners, :locked
   end
+
+  def down
+    remove_index :ci_runners, :locked if index_exists? :ci_runners, :locked
+  end
 end
diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
index 7c991c6d998d29f5f4124eeda64ac67fb1da9911..a05a4c679e3e97eef2b674bbbc6a8f1d3086a203 100644
--- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
+++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
@@ -1,9 +1,15 @@
 class AddIndexForPipelineUserId < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
 
+  DOWNTIME = false
+
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :ci_commits, :user_id
   end
+
+  def down
+    remove_index :ci_commits, :user_id if index_exists? :ci_commits, :user_id
+  end
 end
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
index a853de3abfbed5842ea423efbe77c077d0163028..3f074723b4ac59ac84f4f1380c1cf5ccc983f606 100644
--- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -5,8 +5,15 @@ class AddDeletedAtToNamespaces < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_column :namespaces, :deleted_at, :datetime
+
     add_concurrent_index :namespaces, :deleted_at
   end
+
+  def down
+    remove_index :namespaces, :deleted_at if index_exists? :namespaces, :deleted_at
+
+    remove_column :namespaces, :deleted_at
+  end
 end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
index 10ef42afce18c316cb54296039c8b792dae65c62..6c5d7268e7201ca9a1b9be90d3224bfb147935c1 100644
--- a/db/migrate/20160808085602_add_index_for_build_token.rb
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -6,7 +6,11 @@ class AddIndexForBuildToken < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :ci_builds, :token, unique: true
   end
+
+  def down
+    remove_index :ci_builds, :token, unique: true if index_exists? :ci_builds, :token, unique: true
+  end
 end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
index b6e8bb18e7bf098c17d015323e37e4031457de2b..8f693e97a58959f86db902665c307aea25bce610 100644
--- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -8,7 +8,11 @@ class AddIndexToNoteDiscussionId < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :notes, :discussion_id
   end
+
+  def down
+    remove_index :notes, :discussion_id if index_exists? :notes, :discussion_id
+  end
 end
diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
index f2cf956adc96faa27c3e52891c1ac9f7a2d6113a..bcad3416d043a6bd95685d7813c7fa94550d301e 100644
--- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -9,8 +9,15 @@ class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_column :users, :incoming_email_token, :string
+
     add_concurrent_index :users, :incoming_email_token
   end
+
+  def down
+    remove_index :users, :incoming_email_token if index_exists? :users, :incoming_email_token
+
+    remove_column :users, :incoming_email_token
+  end
 end
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
index 8753e55e058b2ba077d2c6f525b85e7969540fb2..9cb44dfa9f9e9c1d80855d8b5fa6c5be9aca7aad 100644
--- a/db/migrate/20160829114652_add_markdown_cache_columns.rb
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -26,7 +26,7 @@ class AddMarkdownCacheColumns < ActiveRecord::Migration
     projects: [:description],
     releases: [:description],
     snippets: [:title, :content],
-  }
+  }.freeze
 
   def change
     COLUMNS.each do |table, columns|
diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb
index 93f9821bc76e21fc743d401eb892358884cc0078..79a5fb29d6405c0611d68d4f3fcd0b3183f96dc9 100644
--- a/db/migrate/20160831214543_migrate_project_features.rb
+++ b/db/migrate/20160831214543_migrate_project_features.rb
@@ -3,7 +3,7 @@ class MigrateProjectFeatures < ActiveRecord::Migration
 
   DOWNTIME = true
   DOWNTIME_REASON =
-    <<-EOT
+    <<-EOT.freeze
       Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
       a new table called project_features.
     EOT
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index d10f3a6d1046f765ca0283b52a7c6360d4b9a83f..e20e693f3aa736642e251f2f97c81a8d5e808fe5 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -5,9 +5,15 @@ class AddGroupIdToLabels < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_column :labels, :group_id, :integer
     add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
     add_concurrent_index :labels, :group_id
   end
+
+  def down
+    remove_foreign_key :labels, column: :group_id
+    remove_index :labels, :group_id if index_exists? :labels, :group_id
+    remove_column :labels, :group_id
+  end
 end
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
index b5de552b98cfb3e76e410f6bd089bdd65837fe82..19f7b1076a7bece2aad21f925738ebf310373bbb 100644
--- a/db/migrate/20160920160832_add_index_to_labels_title.rb
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsTitle < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :labels, :title
   end
+
+  def down
+    remove_index :labels, :title if index_exists? :labels, :title
+  end
 end
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
index e875213ab96db659ef130a0f60f4e0f03dfc8957..9f502a8df734f5a07b90dbe8e0c1fe187b84869a 100644
--- a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -71,7 +71,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
       'StuckCiBuildsWorker'                     => :cronjob,
       'UpdateMergeRequestsWorker'               => :update_merge_requests
     }
-  }
+  }.freeze
 
   def up
     Sidekiq.redis do |redis|
@@ -93,7 +93,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
 
   def migrate_from_queue(redis, queue, job_mapping)
     while job = redis.lpop("queue:#{queue}")
-      payload = JSON.load(job)
+      payload = JSON.parse(job)
       new_queue = job_mapping[payload['class']]
 
       # If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
index 2abfe47b7766a2da5409e544d2baf919680dd886..35ad22b6c0138c7ad27609523ca7c654c8c6b06d 100644
--- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -25,9 +25,15 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
   # comments:
   # disable_ddl_transaction!
 
-  def change
+  def up
     add_column :merge_request_metrics, :pipeline_id, :integer
-    add_concurrent_index :merge_request_metrics, :pipeline_id
     add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
+    add_concurrent_index :merge_request_metrics, :pipeline_id
+  end
+
+  def down
+    remove_foreign_key :merge_request_metrics, column: :pipeline_id
+    remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id
+    remove_column :merge_request_metrics, :pipeline_id
   end
 end
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
index 06d07bdb83516776da89b94ce2b4a012cabdc76b..fc2e4c12b300a049aa6b1f9357414a7f9dd9329e 100644
--- a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -25,7 +25,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
       incoming_email: {
           'EmailReceiverWorker' => :email_receiver
       }
-  }
+  }.freeze
 
   def up
     Sidekiq.redis do |redis|
@@ -47,7 +47,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
 
   def migrate_from_queue(redis, queue, job_mapping)
     while job = redis.lpop("queue:#{queue}")
-      payload = JSON.load(job)
+      payload = JSON.parse(job)
       new_queue = job_mapping[payload['class']]
 
       # If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
index d5c343dc527bdf4d9685784bab0756e3f906aeef..8b1c10a124fd63ef1463fee88749e4f89f93935c 100644
--- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -9,6 +9,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
   end
 
   def down
+    remove_foreign_key :subscriptions, column: :project_id
     remove_column :subscriptions, :project_id
   end
 end
diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb
index 750a6a8c51ebab2aef1adc088a9593e5e43423dd..94b8ddd46f53739d8136a0c63678673d9f298b31 100644
--- a/db/migrate/20161106185620_add_project_import_data_project_index.rb
+++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb
@@ -6,7 +6,11 @@ class AddProjectImportDataProjectIndex < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :project_import_data, :project_id
   end
+
+  def down
+    remove_index :project_import_data, :project_id if index_exists? :project_import_data, :project_id
+  end
 end
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
index eab74c01dfd89b5fc400b11bc3d17f44f022077d..73f9d92bb2275e365251f9bb26917e76003c023e 100644
--- a/db/migrate/20161124111395_add_index_to_parent_id.rb
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -8,7 +8,11 @@ class AddIndexToParentId < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index(:namespaces, [:parent_id, :id], unique: true)
   end
+
+  def down
+    remove_index :namespaces, [:parent_id, :id] if index_exists? :namespaces, [:parent_id, :id]
+  end
 end
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 77e0c40d850c2b4884f06c6ef13baa256a15d119..e5292cfba079cba9f6279b1879dc625da867860d 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -12,7 +12,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
     end
 
     def repository_storage_path
-      Gitlab.config.repositories.storages[repository_storage]
+      Gitlab.config.repositories.storages[repository_storage]['path']
     end
 
     def repository_path
@@ -34,7 +34,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
       new_jobs = []
 
       while job = redis.lpop('queue:process_commit')
-        payload = JSON.load(job)
+        payload = JSON.parse(job)
         project = Project.find_including_path(payload['args'][0])
 
         next unless project
@@ -75,7 +75,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
       new_jobs = []
 
       while job = redis.lpop('queue:process_commit')
-        payload = JSON.load(job)
+        payload = JSON.parse(job)
 
         payload['args'][2] = payload['args'][2]['id']
 
diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
index 9deab19782e01277768c5cc4d2e3d3b1e59dfecd..8100287ef48b1d7030233e834394b651d055c985 100644
--- a/db/migrate/20161128142110_remove_unnecessary_indexes.rb
+++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
@@ -12,7 +12,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
     remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id)
     remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id)
     remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id)
-    remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"])
+    remove_index :deployments, column: %w(project_id environment_id) if index_exists?(:deployments, %w(project_id environment_id))
     remove_index :lists, column: :board_id if index_exists?(:lists, :board_id)
     remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id)
     remove_index :notes, column: :project_id if index_exists?(:notes, :project_id)
@@ -24,7 +24,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
     add_concurrent_index :award_emoji, :user_id
     add_concurrent_index :ci_builds, :commit_id
     add_concurrent_index :deployments, :project_id
-    add_concurrent_index :deployments, ["project_id", "environment_id"]
+    add_concurrent_index :deployments, %w(project_id environment_id)
     add_concurrent_index :lists, :board_id
     add_concurrent_index :milestones, :project_id
     add_concurrent_index :notes, :project_id
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
index 3ae3f2c159b57c9c09e74ae6db2cbe5693650ce4..82fbdf02444d04fab44dca8e1df33bf13c3ea80d 100644
--- a/db/migrate/20161201160452_migrate_project_statistics.rb
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -16,8 +16,9 @@ class MigrateProjectStatistics < ActiveRecord::Migration
     remove_column :projects, :commit_count
   end
 
+  # rubocop: disable Migration/AddColumn
   def down
-    add_column_with_default :projects, :repository_size, :float, default: 0.0
-    add_column_with_default :projects, :commit_count, :integer, default: 0
+    add_column :projects, :repository_size, :float, default: 0.0
+    add_column :projects, :commit_count, :integer, default: 0
   end
 end
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
index 4a51337bda638c5e004b813ea90e17fca08911b4..6d6c8906204a24173d439d4a4fadb5e626c3a4cf 100644
--- a/db/migrate/20161202152035_add_index_to_routes.rb
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -9,8 +9,13 @@ class AddIndexToRoutes < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index(:routes, :path, unique: true)
     add_concurrent_index(:routes, [:source_type, :source_id], unique: true)
   end
+
+  def down
+    remove_index(:routes, :path) if index_exists? :routes, :path
+    remove_index(:routes, [:source_type, :source_id]) if index_exists? :routes, [:source_type, :source_id]
+  end
 end
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
index b74552e762dcd1657cb335ba50d148074305a529..a20a903a7522cd83a1beba7c2e415308cd3141d1 100644
--- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -42,10 +42,10 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
     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
+          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
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
index ac680c8d10f5334afbf8e45ed0cd64294891e5b9..5ff0f5bae4d4bf3b31328df57977d3b4ad806808 100644
--- a/db/migrate/20161207231621_create_environment_name_unique_index.rb
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -12,7 +12,7 @@ class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
   end
 
   def down
-    remove_index :environments, [:project_id, :name], unique: true
-    add_concurrent_index :environments, [:project_id, :name]
+    remove_index :environments, [:project_id, :name]
+    add_concurrent_index :environments, [:project_id, :name], unique: true
   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
index e9fcef1cd45601d53f132d211c7a743a3d2cdbcd..ede0316e86076c7da96dd01ea66a031e6d6a3b27 100644
--- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -9,7 +9,11 @@ class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :environments, [:project_id, :slug], unique: true
   end
+
+  def down
+    remove_index :environments, [:project_id, :slug] if index_exists? :environments, [:project_id, :slug]
+  end
 end
diff --git a/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e63d5927f86b0d927860d0b019169e16e885e111
--- /dev/null
+++ b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
@@ -0,0 +1,37 @@
+class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    create_table :oauth_openid_requests do |t|
+      t.integer :access_grant_id, null: false
+      t.string :nonce, null: false
+    end
+
+    if Gitlab::Database.postgresql?
+      # add foreign key without validation to avoid downtime on PostgreSQL,
+      # also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
+      execute %q{
+        ALTER TABLE "oauth_openid_requests"
+          ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
+          FOREIGN KEY ("access_grant_id")
+          REFERENCES "oauth_access_grants" ("id")
+          NOT VALID;
+      }
+    else
+      execute %q{
+        ALTER TABLE oauth_openid_requests
+          ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id
+          FOREIGN KEY (access_grant_id)
+          REFERENCES oauth_access_grants (id);
+      }
+    end
+  end
+
+  def down
+    drop_table :oauth_openid_requests
+  end
+end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
index 6958500306f925eaed4769ecc42fb4b3b8cf36d9..53f4c6bbb18a80dea8d1a28954fe51b7752fe122 100644
--- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -17,6 +17,6 @@ class AddLowerPathIndexToRoutes < ActiveRecord::Migration
   def down
     return unless Gitlab::Database.postgresql?
 
-    remove_index :routes, name: :index_on_routes_lower_path
+    remove_index :routes, name: :index_on_routes_lower_path if index_exists?(:routes, name: :index_on_routes_lower_path)
   end
 end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index 241afc6b097a415fd1efea879920b8fd7d392723..8fb1f9d5e737e0230ade8412fb49553731d072ce 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -60,7 +60,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
 
   def move_namespace(group_id, path_was, path)
     repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
-      Gitlab.config.repositories.storages[row['repository_storage']]
+      Gitlab.config.repositories.storages[row['repository_storage']]['path']
     end.compact
 
     # Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index a0ce927161f1e192c2d67e51b8030fa3b597439f..61dcc8c54f5844bda2b52163091ed10cfd367ec9 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -71,7 +71,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
     route_exists = route_exists?(path)
 
     Gitlab.config.repositories.storages.each_value do |storage|
-      if route_exists || path_exists?(path, storage)
+      if route_exists || path_exists?(path, storage['path'])
         counter += 1
         path = "#{base}#{counter}"
 
@@ -84,7 +84,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
 
   def move_namespace(namespace_id, path_was, path)
     repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
-      Gitlab.config.repositories.storages[row['repository_storage']]
+      Gitlab.config.repositories.storages[row['repository_storage']]['path']
     end.compact
 
     # Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af1bac897cc87fdc5ced6c9f867c09d33a394096
--- /dev/null
+++ b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ChangeExpiresAtToDateInPersonalAccessTokens < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration requires downtime because it alters expires_at column from datetime to date'
+
+  def up
+    change_column :personal_access_tokens, :expires_at, :date
+  end
+
+  def down
+    change_column :personal_access_tokens, :expires_at, :datetime
+  end
+end
diff --git a/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea9caceaa2c3a99c0d5758cf680948d281aa6c8e
--- /dev/null
+++ b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddImpersonationToPersonalAccessTokens < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def up
+    add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false, allow_null: false
+  end
+
+  def down
+    remove_column :personal_access_tokens, :impersonation
+  end
+end
diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7995d383986393a3e9b67a14f49639b2c4f747fc
--- /dev/null
+++ b/db/migrate/20170120131253_create_chat_teams.rb
@@ -0,0 +1,18 @@
+class CreateChatTeams < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = "Adding a foreign key"
+
+  disable_ddl_transaction!
+
+  def change
+    create_table :chat_teams do |t|
+      t.references :namespace, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+      t.string :team_id
+      t.string :name
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
deleted file mode 100644
index f90637e1e35757b8a29928dbb1df2a52fcf46aac..0000000000000000000000000000000000000000
--- a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class AddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
-  include Gitlab::Database::MigrationHelpers
-  disable_ddl_transaction!
-
-  DOWNTIME = false
-
-  def up
-    add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
-  end
-
-  def down
-    remove_column :users, :notified_of_own_activity
-  end
-end
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
index 69bfa2d3fc4dcd1c418588c25161a2e21c684641..a7d4e141a1aae27e12e3726131dc3b48028c9a18 100644
--- a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -49,6 +49,9 @@ class AddForeignKeysToTimelogs < ActiveRecord::Migration
     Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
     Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
 
+    remove_foreign_key :timelogs, name: 'fk_timelogs_issues_issue_id'
+    remove_foreign_key :timelogs, name: 'fk_timelogs_merge_requests_merge_request_id'
+
     remove_columns :timelogs, :issue_id, :merge_request_id
   end
 end
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
index e9a0aee4d6a62d2828b02e07a044b9ce8daff01b..629b49436e3ac0e2ccf73c0cd40224dd9c9c7a0d 100644
--- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -8,4 +8,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
   def up
     add_concurrent_index(:project_authorizations, :project_id)
   end
+
+  def down
+    remove_index(:project_authorizations, :project_id) if
+      Gitlab::Database.postgresql?
+  end
 end
diff --git a/db/migrate/20170130221926_create_uploads.rb b/db/migrate/20170130221926_create_uploads.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f06c5dd84082bfe37a11118c0c61d72036956e1
--- /dev/null
+++ b/db/migrate/20170130221926_create_uploads.rb
@@ -0,0 +1,20 @@
+class CreateUploads < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    create_table :uploads do |t|
+      t.integer :size, limit: 8, null: false
+      t.string :path, null: false
+      t.string :checksum, limit: 64
+      t.references :model, polymorphic: true
+      t.string :uploader, null: false
+      t.datetime :created_at, null: false
+    end
+
+    add_index :uploads, :path
+    add_index :uploads, :checksum
+    add_index :uploads, [:model_id, :model_type]
+  end
+end
diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1baad0893e3454cfec2124a63e36bddd01e62a76
--- /dev/null
+++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRelativePositionToIssues < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  disable_ddl_transaction!
+
+  def up
+    add_column :issues, :relative_position, :integer
+
+    add_concurrent_index :issues, :relative_position
+  end
+
+  def down
+    remove_column :issues, :relative_position
+
+    remove_index :issues, :relative_position if index_exists? :issues, :relative_position
+  end
+end
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
index 8f944930807afc93fa90acbab92c6cb7ba0f1621..31ef458c44f6ef3bf2ecbf7fd4269ec633b044ed 100644
--- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :labels, [:type, :project_id]
   end
+
+  def down
+    remove_index :labels, [:type, :project_id] if index_exists? :labels, [:type, :project_id]
+  end
 end
diff --git a/db/migrate/20170206115204_add_column_ghost_to_users.rb b/db/migrate/20170206115204_add_column_ghost_to_users.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc1eeda1160f11fab73cf3e53adda376a7a1bbd6
--- /dev/null
+++ b/db/migrate/20170206115204_add_column_ghost_to_users.rb
@@ -0,0 +1,11 @@
+class AddColumnGhostToUsers < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def up
+    add_column :users, :ghost, :boolean
+  end
+
+  def down
+    remove_column :users, :ghost
+  end
+end
diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
index f922ed209aa3f2618f17bf02bcc90650f5d4f847..70fb0ef12f9bf528b97b7e7e0f6d5f432020515b 100644
--- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
+++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
@@ -5,8 +5,13 @@ class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :labels, :title
     add_concurrent_index :labels, :project_id
   end
+
+  def down
+    remove_index :labels, :title if index_exists? :labels, :title
+    remove_index :labels, :project_id if index_exists? :labels, :project_id
+  end
 end
diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
index 61e49c14fc0d2edb9344cf1a34975577e2798661..07d4f8af27fe5fa59c06e04579b5a224af339058 100644
--- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -5,7 +5,11 @@ class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
+  def up
     add_concurrent_index :ci_trigger_requests, :commit_id
   end
+
+  def down
+    remove_index :ci_trigger_requests, :commit_id if index_exists? :ci_trigger_requests, :commit_id
+  end
 end
diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
index c01753cfbd216721eaf7c7a88b4dd3b8e368be7f..2d8329b7862179b2e31883d63a356727776e3b6a 100644
--- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
@@ -8,7 +8,11 @@ class AddIndexToUserAgentDetail < ActiveRecord::Migration
 
   disable_ddl_transaction!
 
-  def change
-    add_concurrent_index(:user_agent_details, [:subject_id, :subject_type])
+  def up
+    add_concurrent_index :user_agent_details, [:subject_id, :subject_type]
+  end
+
+  def down
+    remove_index :user_agent_details, [:subject_id, :subject_type] if index_exists? :user_agent_details, [:subject_id, :subject_type]
   end
 end
diff --git a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ab970134be83578c95a033679b86e24c43e8d13
--- /dev/null
+++ b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb
@@ -0,0 +1,17 @@
+class AddUniqueIpsLimitToApplicationSettings < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  DOWNTIME = false
+  disable_ddl_transaction!
+
+  def up
+    add_column :application_settings, :unique_ips_limit_per_user, :integer
+    add_column :application_settings, :unique_ips_limit_time_window, :integer
+    add_column_with_default :application_settings, :unique_ips_limit_enabled, :boolean, default: false
+  end
+
+  def down
+    remove_column :application_settings, :unique_ips_limit_per_user
+    remove_column :application_settings, :unique_ips_limit_time_window
+    remove_column :application_settings, :unique_ips_limit_enabled
+  end
+end
diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0e3ff8957a05f8c4f1b3c6fdbc436294b0bd844
--- /dev/null
+++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
@@ -0,0 +1,11 @@
+class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :application_settings,
+      :default_artifacts_expire_in, :string,
+      null: false, default: '0'
+  end
+end
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8a96a784c9727ed0e4d96e0854be03d7d11f205c
--- /dev/null
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -0,0 +1,14 @@
+class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
+  end
+
+  def down
+    remove_index :ci_commits, [:gl_project_id, :ref, :status] if index_exists? :ci_commits, [:gl_project_id, :ref, :status]
+  end
+end
diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a2839f52d89c102934c7979f82b8810a3b160a61
--- /dev/null
+++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
@@ -0,0 +1,8 @@
+class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  DOWNTIME = false
+
+  def change
+    remove_index(:ci_commits, column: [:gl_project_id, :status])
+  end
+end
diff --git a/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9011526565dcf51bd2790549b661a9764e916344
--- /dev/null
+++ b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameMergeWhenBuildSucceeds < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = true
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  DOWNTIME_REASON = 'Renaming the column merge_when_build_succeeds'
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  # disable_ddl_transaction!
+
+  def change
+    rename_column :merge_requests, :merge_when_build_succeeds, :merge_when_pipeline_succeeds
+  end
+end
diff --git a/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b2b68ff72d134b25cf835df89ac56a7076965d52
--- /dev/null
+++ b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameOnlyAllowMergeIfBuildSucceeds < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = true
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  DOWNTIME_REASON = 'Renaming the column only_allow_merge_if_build_succeeds'
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  # disable_ddl_transaction!
+
+  def change
+    rename_column :projects, :only_allow_merge_if_build_succeeds, :only_allow_merge_if_pipeline_succeeds
+  end
+end
diff --git a/db/migrate/20170217151948_add_owner_id_to_triggers.rb b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16d7cc5bed69189e381083db38085556a5ff55c0
--- /dev/null
+++ b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddOwnerIdToTriggers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_triggers, :owner_id, :integer
+  end
+end
diff --git a/db/migrate/20170217151949_add_description_to_triggers.rb b/db/migrate/20170217151949_add_description_to_triggers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1dca0e374124a005e4dfc81d40016f706b7bd18c
--- /dev/null
+++ b/db/migrate/20170217151949_add_description_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddDescriptionToTriggers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_triggers, :description, :string
+  end
+end
diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4db8658f36fbe1c3edcc128ebc492174eb14ab22
--- /dev/null
+++ b/db/migrate/20170222143317_drop_ci_projects.rb
@@ -0,0 +1,34 @@
+class DropCiProjects < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    drop_table :ci_projects
+  end
+
+  def down
+    create_table "ci_projects", force: :cascade do |t|
+      t.string "name"
+      t.integer "timeout", default: 3600, null: false
+      t.datetime "created_at"
+      t.datetime "updated_at"
+      t.string "token"
+      t.string "default_ref"
+      t.string "path"
+      t.boolean "always_build", default: false, null: false
+      t.integer "polling_interval"
+      t.boolean "public", default: false, null: false
+      t.string "ssh_url_to_repo"
+      t.integer "gitlab_id"
+      t.boolean "allow_git_fetch", default: true, null: false
+      t.string "email_recipients", default: "", null: false
+      t.boolean "email_add_pusher", default: true, null: false
+      t.boolean "email_only_broken_builds", default: true, null: false
+      t.string "skip_refs"
+      t.string "coverage_regex"
+      t.boolean "shared_runners_enabled", default: false
+      t.text "generated_yaml_config"
+    end
+  end
+end
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eac93e8e407c1c9eb5155db895c466d3e0ac1cd4
--- /dev/null
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -0,0 +1,28 @@
+class RemoveOldProjectIdColumns < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Unused columns are being removed.'
+
+  def up
+    remove_index :ci_builds, :project_id if
+      index_exists?(:ci_builds, :project_id)
+
+    remove_column :ci_builds, :project_id
+    remove_column :ci_commits, :project_id
+    remove_column :ci_runner_projects, :project_id
+    remove_column :ci_triggers, :project_id
+    remove_column :ci_variables, :project_id
+  end
+
+  def down
+    add_column :ci_builds, :project_id, :integer
+    add_column :ci_commits, :project_id, :integer
+    add_column :ci_runner_projects, :project_id, :integer
+    add_column :ci_triggers, :project_id, :integer
+    add_column :ci_variables, :project_id, :integer
+
+    add_concurrent_index :ci_builds, :project_id
+  end
+end
diff --git a/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c19d471557811cccd05bcb9c388710cad7a518f
--- /dev/null
+++ b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
@@ -0,0 +1,14 @@
+class RenameGlProjectIdToProjectId < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Renaming an actively used column.'
+
+  def change
+    rename_column :ci_builds, :gl_project_id, :project_id
+    rename_column :ci_commits, :gl_project_id, :project_id
+    rename_column :ci_runner_projects, :gl_project_id, :project_id
+    rename_column :ci_triggers, :gl_project_id, :project_id
+    rename_column :ci_variables, :gl_project_id, :project_id
+  end
+end
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f54608ecceb55b3f287da9c7d393c5a910037c1b
--- /dev/null
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def up
+    add_column_with_default(:projects, :printing_merge_request_link_enabled, :boolean, default: true)
+  end
+
+  def down
+    remove_column(:projects, :printing_merge_request_link_enabled)
+  end
+end
diff --git a/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f061d9639214562447a7190311302110ab286f4
--- /dev/null
+++ b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
@@ -0,0 +1,10 @@
+class RenameCiCommitsToCiPipelines < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Rename table ci_commits to ci_pipelines'
+
+  def change
+    rename_table 'ci_commits', 'ci_pipelines'
+  end
+end
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e2abea5254c7e2aae899db165445fea265ad052
--- /dev/null
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -0,0 +1,83 @@
+class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON =
+    'Remove unused columns in used tables.' \
+    ' Downtime required in case Rails caches them'
+
+  def up
+    %w[ci_application_settings
+       ci_events
+       ci_jobs
+       ci_sessions
+       ci_taggings
+       ci_tags].each do |table|
+      drop_table(table)
+    end
+
+    remove_column :ci_pipelines, :push_data, :text
+    remove_column :ci_builds, :job_id, :integer
+    remove_column :ci_builds, :deploy, :boolean
+  end
+
+  def down
+    add_column :ci_builds, :deploy, :boolean
+    add_column :ci_builds, :job_id, :integer
+    add_column :ci_pipelines, :push_data, :text
+
+    create_table "ci_tags", force: :cascade do |t|
+      t.string "name"
+      t.integer "taggings_count", default: 0
+    end
+
+    create_table "ci_taggings", force: :cascade do |t|
+      t.integer "tag_id"
+      t.integer "taggable_id"
+      t.string "taggable_type"
+      t.integer "tagger_id"
+      t.string "tagger_type"
+      t.string "context", limit: 128
+      t.datetime "created_at"
+    end
+
+    add_index "ci_taggings", %w[taggable_id taggable_type context]
+
+    create_table "ci_sessions", force: :cascade do |t|
+      t.string "session_id", null: false
+      t.text "data"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+
+    create_table "ci_jobs", force: :cascade do |t|
+      t.integer "project_id", null: false
+      t.text "commands"
+      t.boolean "active", default: true, null: false
+      t.datetime "created_at"
+      t.datetime "updated_at"
+      t.string "name"
+      t.boolean "build_branches", default: true, null: false
+      t.boolean "build_tags", default: false, null: false
+      t.string "job_type", default: "parallel"
+      t.string "refs"
+      t.datetime "deleted_at"
+    end
+
+    create_table "ci_events", force: :cascade do |t|
+      t.integer "project_id"
+      t.integer "user_id"
+      t.integer "is_admin"
+      t.text "description"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+
+    create_table "ci_application_settings", force: :cascade do |t|
+      t.boolean "all_broken_builds"
+      t.boolean "add_pusher"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+  end
+end
diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fbdc45f1a748e0ba19942413fc951ee0bbf626f
--- /dev/null
+++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
@@ -0,0 +1,15 @@
+class AddOwnerIdForeignKey < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade
+  end
+
+  def down
+    remove_foreign_key :ci_triggers, column: :owner_id
+  end
+end
diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c429039c275a299188741cba4b55479a9f65b3f8
--- /dev/null
+++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb
@@ -0,0 +1,24 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToUserGhost < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :users, :ghost
+  end
+
+  def down
+    remove_index :users, :ghost
+  end
+end
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b39c0a3be0f350a6f98ce3d854ea47e72b0eae44
--- /dev/null
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -0,0 +1,24 @@
+class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    if our_column_exists?
+      remove_column :users, :notified_of_own_activity
+    end
+  end
+
+  def down
+    unless our_column_exists?
+      add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
+    end
+  end
+
+  private
+
+  def our_column_exists?
+    column_exists?(:users, :notified_of_own_activity)
+  end
+end
diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1326118cc8dacec0e9a47b641e86b48978a81036
--- /dev/null
+++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb
@@ -0,0 +1,7 @@
+class AddClosedAtToIssues < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def change
+    add_column :issues, :closed_at, :datetime
+  end
+end
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
index df38591a33388be2f4dbea6f5a20dd70f6a3ad04..14b5ef476f0cbbf8a5d73ed09b09081073e6341d 100644
--- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -14,15 +14,15 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
 
     finder_sql =
       projects.
-      join(namespaces, Arel::Nodes::InnerJoin).
-      on(projects[:namespace_id].eq(namespaces[:id])).
-      where(projects[:visibility_level].gt(namespaces[:visibility_level])).
-      project(projects[:id], namespaces[:visibility_level]).
-      take(BATCH_SIZE).
-      to_sql
+        join(namespaces, Arel::Nodes::InnerJoin).
+        on(projects[:namespace_id].eq(namespaces[:id])).
+        where(projects[:visibility_level].gt(namespaces[:visibility_level])).
+        project(projects[:id], namespaces[:visibility_level]).
+        take(BATCH_SIZE).
+        to_sql
 
     # Update matching rows in batches. Each batch can cause up to 3 UPDATE
-    # statements, in addition to the SELECT: one per visibility_level 
+    # statements, in addition to the SELECT: one per visibility_level
     loop do
       to_update = connection.exec_query(finder_sql)
       break if to_update.rows.count == 0
diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
index 282837be1fa3d7bc74934500e4edb55e70fc36ae..49a6bc884a8320f1fc4e9c335c91ec9161e80952 100644
--- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb
+++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
@@ -37,7 +37,7 @@ class RenameReservedProjectNames < ActiveRecord::Migration
                    unsubscribes
                    update
                    users
-                   wikis)
+                   wikis).freeze
 
   def up
     queues = Array.new(THREAD_COUNT) { Queue.new }
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b518038e93a9c6c381decaeaf913c65f0535427b
--- /dev/null
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    # This ensures we don't lock all users for the duration of the migration.
+    update_column_in_batches(:users, :authorized_projects_populated, nil)
+  end
+
+  def down
+    # noop
+  end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
index 89aa753646c64975eb648f66bce56b589344ba42..aee0c1b6245160cc1ed38f0e883786367f97819c 100644
--- a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -18,6 +18,7 @@ class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
   # disable_ddl_transaction!
 
   def change
-    remove_columns :timelogs, :trackable_id, :trackable_type
+    remove_column :timelogs, :trackable_id, :integer
+    remove_column :timelogs, :trackable_type, :string
   end
 end
diff --git a/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e206f9af63601ca54bb1b70bcd4cced4d97b1c8c
--- /dev/null
+++ b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
@@ -0,0 +1,20 @@
+class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    if Gitlab::Database.postgresql?
+      execute %q{
+        ALTER TABLE "oauth_openid_requests"
+          VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id";
+      }
+    end
+  end
+
+  def down
+    # noop
+  end
+end
diff --git a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
index 84954b1ef641c99ecf5eb55bd5fa292e50fdbef1..603efc43782f49d243a8e10afe7a882a77a43332 100644
--- a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
+++ b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
@@ -1,10 +1,8 @@
 class DisableInvalidServiceTemplates < ActiveRecord::Migration
   DOWNTIME = false
 
-  unless defined?(Service)
-    class Service < ActiveRecord::Base
-      self.inheritance_column = nil
-    end
+  class Service < ActiveRecord::Base
+    self.inheritance_column = nil
   end
 
   def up
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2dd14ee5a787eebe2f393dc0b047c84772f45e64
--- /dev/null
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -0,0 +1,87 @@
+class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  include Gitlab::Database
+
+  DOWNTIME = false
+
+  def up
+    Gitlab::Database.with_connection_pool(2) do |pool|
+      threads = []
+
+      threads << Thread.new do
+        pool.with_connection do |connection|
+          Thread.current[:foreign_key_connection] = connection
+
+          execute(<<-SQL.strip_heredoc)
+            UPDATE services
+              SET properties = replace(properties,
+                                       'notify_only_broken_builds',
+                                       'notify_only_broken_pipelines')
+                , pipeline_events = #{true_value}
+                , build_events = #{false_value}
+            WHERE type IN
+              ('SlackService', 'MattermostService', 'HipchatService')
+              AND build_events = #{true_value};
+          SQL
+        end
+      end
+
+      threads << Thread.new do
+        pool.with_connection do |connection|
+          Thread.current[:foreign_key_connection] = connection
+
+          execute(update_pipeline_services_sql)
+        end
+      end
+
+      threads.each(&:join)
+    end
+  end
+
+  def down
+    # Don't bother to migrate the data back
+  end
+
+  def connection
+    # Rails memoizes connection objects, but this causes them to be shared
+    # amongst threads; we don't want that.
+    Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
+  end
+
+  private
+
+  def update_pipeline_services_sql
+    if Gitlab::Database.postgresql?
+      <<-SQL
+        UPDATE services
+          SET type = 'PipelinesEmailService'
+            , properties = replace(properties,
+                                   'notify_only_broken_builds',
+                                   'notify_only_broken_pipelines')
+            , pipeline_events = #{true_value}
+            , build_events = #{false_value}
+        WHERE type = 'BuildsEmailService'
+        AND
+          (SELECT 1 FROM services pipeline_services
+             WHERE pipeline_services.project_id = services.project_id
+               AND pipeline_services.type = 'PipelinesEmailService' LIMIT 1)
+          IS NULL;
+      SQL
+    else
+      <<-SQL
+        UPDATE services build_services
+         LEFT OUTER JOIN services pipeline_services
+           ON build_services.project_id = pipeline_services.project_id
+          AND pipeline_services.type = 'PipelinesEmailService'
+          SET build_services.type = 'PipelinesEmailService'
+            , build_services.properties = replace(build_services.properties,
+                                         'notify_only_broken_builds',
+                                         'notify_only_broken_pipelines')
+            , build_services.pipeline_events = #{true_value}
+            , build_services.build_events = #{false_value}
+        WHERE build_services.type = 'BuildsEmailService'
+          AND pipeline_services.id IS NULL;
+      SQL
+    end.strip_heredoc
+  end
+end
diff --git a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec6e8cdfc456a27c83f57f7e3444ba9dd7c153f3
--- /dev/null
+++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
@@ -0,0 +1,23 @@
+class MigrateLegacyManualActions < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    disable_statement_timeout
+
+    execute <<-EOS
+      UPDATE ci_builds SET status = 'manual', allow_failure = true
+        WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped';
+    EOS
+  end
+
+  def down
+    disable_statement_timeout
+
+    execute <<-EOS
+      UPDATE ci_builds SET status = 'skipped', allow_failure = false
+        WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual';
+    EOS
+  end
+end
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b61dd7cfc618d528a59b4b2b277cbab581a483cf
--- /dev/null
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetRelativePositionForIssue < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    update_column_in_batches(:issues, :relative_position, nil) do |table, query|
+      query.where(table[:relative_position].not_eq(nil))
+    end
+  end
+
+  def down
+  end
+end
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44c688fa134ca7e9a497d8379c846f6b97c972aa
--- /dev/null
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -0,0 +1,72 @@
+require 'thread'
+
+class RenameMoreReservedProjectNames < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  include Gitlab::ShellAdapter
+
+  DOWNTIME = false
+
+  KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
+
+  def up
+    reserved_projects.each_slice(100) do |slice|
+      rename_projects(slice)
+    end
+  end
+
+  def down
+    # nothing to do here
+  end
+
+  private
+
+  def reserved_projects
+    Project.unscoped.
+      includes(:namespace).
+      where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)').
+      where('projects.path' => KNOWN_PATHS)
+  end
+
+  def route_exists?(full_path)
+    quoted_path = ActiveRecord::Base.connection.quote_string(full_path)
+
+    ActiveRecord::Base.connection.
+      select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present?
+  end
+
+  # Adds number to the end of the path that is not taken by other route
+  def rename_path(namespace_path, path_was)
+    counter = 0
+    path = "#{path_was}#{counter}"
+
+    while route_exists?("#{namespace_path}/#{path}")
+      counter += 1
+      path = "#{path_was}#{counter}"
+    end
+
+    path
+  end
+
+  def rename_projects(projects)
+    projects.each do |project|
+      id = project.id
+      path_was = project.path
+      namespace_path = project.namespace.path
+      path = rename_path(namespace_path, path_was)
+
+      begin
+        # Because project path update is quite complex operation we can't safely
+        # copy-paste all code from GitLab. As exception we use Rails code here
+        project.rename_repo if rename_project_row(project, path)
+      rescue Exception => e # rubocop: disable Lint/RescueException
+        Rails.logger.error "Exception when renaming project #{id}: #{e.message}"
+      end
+    end
+  end
+
+  def rename_project_row(project, path)
+    project.respond_to?(:update_attributes) &&
+      project.update_attributes(path: path) &&
+      project.respond_to?(:rename_repo)
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 08d115468003df7ef1c4a5ade00618e14aa75881..db57fb0a548ea8010fcabb9979628c9f66d30793 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: 20170215200045) do
+ActiveRecord::Schema.define(version: 20170315194013) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -112,6 +112,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.string "plantuml_url"
     t.boolean "plantuml_enabled"
     t.integer "terminal_max_session_time", default: 0, null: false
+    t.integer "max_pages_size", default: 100, null: false
+    t.string "default_artifacts_expire_in", default: "0", null: false
+    t.integer "unique_ips_limit_per_user"
+    t.integer "unique_ips_limit_time_window"
+    t.boolean "unique_ips_limit_enabled", default: false, null: false
   end
 
   create_table "audit_events", force: :cascade do |t|
@@ -172,15 +177,17 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
   add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
 
-  create_table "ci_application_settings", force: :cascade do |t|
-    t.boolean "all_broken_builds"
-    t.boolean "add_pusher"
-    t.datetime "created_at"
-    t.datetime "updated_at"
+  create_table "chat_teams", force: :cascade do |t|
+    t.integer "namespace_id", null: false
+    t.string "team_id"
+    t.string "name"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
+  add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+
   create_table "ci_builds", force: :cascade do |t|
-    t.integer "project_id"
     t.string "status"
     t.datetime "finished_at"
     t.text "trace"
@@ -191,9 +198,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.float "coverage"
     t.integer "commit_id"
     t.text "commands"
-    t.integer "job_id"
     t.string "name"
-    t.boolean "deploy", default: false
     t.text "options"
     t.boolean "allow_failure", default: false, null: false
     t.string "stage"
@@ -206,7 +211,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.string "target_url"
     t.string "description"
     t.text "artifacts_file"
-    t.integer "gl_project_id"
+    t.integer "project_id"
     t.text "artifacts_metadata"
     t.integer "erased_by_id"
     t.datetime "erased_at"
@@ -225,25 +230,22 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
   add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
   add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
-  add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
   add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
   add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
   add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
   add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
   add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
 
-  create_table "ci_commits", force: :cascade do |t|
-    t.integer "project_id"
+  create_table "ci_pipelines", force: :cascade do |t|
     t.string "ref"
     t.string "sha"
     t.string "before_sha"
-    t.text "push_data"
     t.datetime "created_at"
     t.datetime "updated_at"
     t.boolean "tag", default: false
     t.text "yaml_errors"
     t.datetime "committed_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
     t.string "status"
     t.datetime "started_at"
     t.datetime "finished_at"
@@ -252,67 +254,20 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.integer "lock_version"
   end
 
-  add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
-  add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
-  add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
-  add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
-  add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
-
-  create_table "ci_events", force: :cascade do |t|
-    t.integer "project_id"
-    t.integer "user_id"
-    t.integer "is_admin"
-    t.text "description"
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
-  create_table "ci_jobs", force: :cascade do |t|
-    t.integer "project_id", null: false
-    t.text "commands"
-    t.boolean "active", default: true, null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-    t.string "name"
-    t.boolean "build_branches", default: true, null: false
-    t.boolean "build_tags", default: false, null: false
-    t.string "job_type", default: "parallel"
-    t.string "refs"
-    t.datetime "deleted_at"
-  end
-
-  create_table "ci_projects", force: :cascade do |t|
-    t.string "name"
-    t.integer "timeout", default: 3600, null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-    t.string "token"
-    t.string "default_ref"
-    t.string "path"
-    t.boolean "always_build", default: false, null: false
-    t.integer "polling_interval"
-    t.boolean "public", default: false, null: false
-    t.string "ssh_url_to_repo"
-    t.integer "gitlab_id"
-    t.boolean "allow_git_fetch", default: true, null: false
-    t.string "email_recipients", default: "", null: false
-    t.boolean "email_add_pusher", default: true, null: false
-    t.boolean "email_only_broken_builds", default: true, null: false
-    t.string "skip_refs"
-    t.string "coverage_regex"
-    t.boolean "shared_runners_enabled", default: false
-    t.text "generated_yaml_config"
-  end
+  add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+  add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+  add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+  add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
+  add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
 
   create_table "ci_runner_projects", force: :cascade do |t|
     t.integer "runner_id", null: false
-    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
   end
 
-  add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
+  add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
   add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
 
   create_table "ci_runners", force: :cascade do |t|
@@ -336,30 +291,6 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
   add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
 
-  create_table "ci_sessions", force: :cascade do |t|
-    t.string "session_id", null: false
-    t.text "data"
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
-  create_table "ci_taggings", force: :cascade do |t|
-    t.integer "tag_id"
-    t.integer "taggable_id"
-    t.string "taggable_type"
-    t.integer "tagger_id"
-    t.string "tagger_type"
-    t.string "context", limit: 128
-    t.datetime "created_at"
-  end
-
-  add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
-  create_table "ci_tags", force: :cascade do |t|
-    t.string "name"
-    t.integer "taggings_count", default: 0
-  end
-
   create_table "ci_trigger_requests", force: :cascade do |t|
     t.integer "trigger_id", null: false
     t.text "variables"
@@ -372,26 +303,26 @@ ActiveRecord::Schema.define(version: 20170215200045) do
 
   create_table "ci_triggers", force: :cascade do |t|
     t.string "token"
-    t.integer "project_id"
     t.datetime "deleted_at"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
+    t.integer "owner_id"
+    t.string "description"
   end
 
-  add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
+  add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
 
   create_table "ci_variables", force: :cascade do |t|
-    t.integer "project_id"
     t.string "key"
     t.text "value"
     t.text "encrypted_value"
     t.string "encrypted_value_salt"
     t.string "encrypted_value_iv"
-    t.integer "gl_project_id"
+    t.integer "project_id"
   end
 
-  add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
+  add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
 
   create_table "container_images", force: :cascade do |t|
     t.integer "project_id"
@@ -520,6 +451,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.text "title_html"
     t.text "description_html"
     t.integer "time_estimate"
+    t.integer "relative_position"
+    t.datetime "closed_at"
   end
 
   add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -531,6 +464,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
   add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
   add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
+  add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
   add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
   add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
@@ -694,7 +628,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.integer "updated_by_id"
     t.text "merge_error"
     t.text "merge_params"
-    t.boolean "merge_when_build_succeeds", default: false, null: false
+    t.boolean "merge_when_pipeline_succeeds", default: false, null: false
     t.integer "merge_user_id"
     t.string "merge_commit_sha"
     t.datetime "deleted_at"
@@ -868,6 +802,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
   add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
 
+  create_table "oauth_openid_requests", force: :cascade do |t|
+    t.integer "access_grant_id", null: false
+    t.string "nonce", null: false
+  end
+
   create_table "pages_domains", force: :cascade do |t|
     t.integer "project_id"
     t.text "certificate"
@@ -884,10 +823,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.string "token", null: false
     t.string "name", null: false
     t.boolean "revoked", default: false
-    t.datetime "expires_at"
+    t.date "expires_at"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
     t.string "scopes", default: "--- []\n", null: false
+    t.boolean "impersonation", default: false, null: false
   end
 
   add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
@@ -977,7 +917,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.boolean "last_repository_check_failed"
     t.datetime "last_repository_check_at"
     t.boolean "container_registry_enabled"
-    t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+    t.boolean "only_allow_merge_if_pipeline_succeeds", default: false, null: false
     t.boolean "has_external_issue_tracker"
     t.string "repository_storage", default: "default", null: false
     t.boolean "request_access_enabled", default: false, null: false
@@ -985,6 +925,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.boolean "lfs_enabled"
     t.text "description_html"
     t.boolean "only_allow_merge_if_all_discussions_are_resolved"
+    t.boolean "printing_merge_request_link_enabled", default: true, null: false
   end
 
   add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1214,6 +1155,20 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
   add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
 
+  create_table "uploads", force: :cascade do |t|
+    t.integer "size", limit: 8, null: false
+    t.string "path", null: false
+    t.string "checksum", limit: 64
+    t.integer "model_id"
+    t.string "model_type"
+    t.string "uploader", null: false
+    t.datetime "created_at", null: false
+  end
+
+  add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
+  add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
+  add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
+
   create_table "user_agent_details", force: :cascade do |t|
     t.string "user_agent", null: false
     t.string "ip_address", null: false
@@ -1286,7 +1241,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
     t.string "incoming_email_token"
     t.string "organization"
     t.boolean "authorized_projects_populated"
-    t.boolean "notified_of_own_activity", default: false, null: false
+    t.boolean "ghost"
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1296,6 +1251,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
   add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
   add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+  add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
   add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
   add_index "users", ["name"], name: "index_users_on_name", using: :btree
   add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -1337,16 +1293,19 @@ ActiveRecord::Schema.define(version: 20170215200045) do
   add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
 
   add_foreign_key "boards", "projects"
+  add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+  add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
   add_foreign_key "issue_metrics", "issues", on_delete: :cascade
   add_foreign_key "label_priorities", "labels", on_delete: :cascade
   add_foreign_key "label_priorities", "projects", on_delete: :cascade
   add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
   add_foreign_key "lists", "boards"
   add_foreign_key "lists", "labels"
-  add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
+  add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
   add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
   add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
   add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
+  add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
   add_foreign_key "personal_access_tokens", "users"
   add_foreign_key "project_authorizations", "projects", on_delete: :cascade
   add_foreign_key "project_authorizations", "users", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 2712206373d4c1b1a303f0340055c76f04f633d3..57d85d770e72d41f042de965722e36d62f4f4246 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -19,7 +19,7 @@
 - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
 - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
 - [Profile Settings](profile/README.md)
-- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat.
+- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat.
 - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
 - [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
 - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
@@ -51,6 +51,7 @@
 - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
 - [Update](update/README.md) Update guides to upgrade your installation.
 - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
+- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
 - [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
 - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
 - [Git LFS configuration](workflow/lfs/lfs_administration.md)
diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md
new file mode 100644
index 0000000000000000000000000000000000000000..2c289c67a6d0e60cd836dd5b7007497a4f0bca70
--- /dev/null
+++ b/doc/administration/auth/crowd.md
@@ -0,0 +1,68 @@
+# Atlassian Crowd OmniAuth Provider
+
+## Configure a new Crowd application
+
+1. Choose 'Applications' in the top menu, then 'Add application'.
+1. Go through the 'Add application' steps, entering the appropriate details.
+   The screenshot below shows an example configuration.
+
+    ![Example Crowd application configuration](img/crowd_application.png)
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file.
+
+    **Omnibus:**
+
+    ```sh
+      sudo editor /etc/gitlab/gitlab.rb
+    ```
+
+    **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. Add the provider configuration:
+
+    **Omnibus:**
+
+    ```ruby
+      gitlab_rails['omniauth_providers'] = [
+        {
+          "name" => "crowd",
+          "args" => {
+            "crowd_server_url" => "CROWD_SERVER_URL",
+            "application_name" => "YOUR_APP_NAME",
+            "application_password" => "YOUR_APP_PASSWORD"
+          }
+        }
+      ]
+    ```
+
+    **Source:**
+
+    ```
+       - { name: 'crowd',
+           args: {
+             crowd_server_url: 'CROWD_SERVER_URL',
+             application_name: 'YOUR_APP_NAME',
+             application_password: 'YOUR_APP_PASSWORD' } }
+    ```
+1. Change `CROWD_SERVER_URL` to the URL of your Crowd server.
+1. Change `YOUR_APP_NAME` to the application name from Crowd applications page.
+1. Change `YOUR_APP_PASSWORD` to the application password you've set.
+1. Save the configuration file.
+1. [Reconfigure][] or [restart][] for the changes to take effect if you
+   installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be a Crowd tab in the sign in form.
+
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/auth/img/crowd_application.png b/doc/administration/auth/img/crowd_application.png
new file mode 100644
index 0000000000000000000000000000000000000000..7deea9dac8e3abe3fe886f86a5d3c04929bac823
Binary files /dev/null and b/doc/administration/auth/img/crowd_application.png differ
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 4d1cb391e697fdae257e64b6195264010be6eaaf..dc4e57f25fbaee2dc5c2fe7d07d1660d6fa08ed7 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -483,12 +483,108 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
 project, you can disable it from your project's settings. Read the user guide
 on how to achieve that.
 
+## Disable Container Registry but use GitLab as an auth endpoint
+
+You can disable the embedded Container Registry to use an external one, but
+still use GitLab as an auth endpoint.
+
+**Omnibus GitLab**
+1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
+
+    ```ruby
+    registry['enable'] = false
+    gitlab_rails['registry_enabled'] = true
+    gitlab_rails['registry_host'] = "registry.gitlab.example.com"
+    gitlab_rails['registry_port'] = "5005"
+    gitlab_rails['registry_api_url'] = "http://localhost:5000"
+    gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
+    gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
+    gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
+    ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`:
+
+    ```
+    ## Container Registry
+
+    registry:
+      enabled: true
+      host: "registry.gitlab.example.com"
+      port: "5005"
+      api_url: "http://localhost:5000"
+      path: /var/opt/gitlab/gitlab-rails/shared/registry
+      key: /var/opt/gitlab/gitlab-rails/certificate.key
+      issuer: omnibus-gitlab-issuer
+    ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
 ## Storage limitations
 
 Currently, there is no storage limitation, which means a user can upload an
 infinite amount of Docker images with arbitrary sizes. This setting will be
 configurable in future releases.
 
+## Configure Container Registry notifications
+
+You can configure the Container Registry to send webhook notifications in 
+response to events happening within the registry.  
+
+Read more about the Container Registry notifications config options in the
+[Docker Registry notifications documentation][notifications-config].
+
+>**Note:**
+Multiple endpoints can be configured for the Container Registry.
+
+
+**Omnibus GitLab installations**
+
+To configure a notification endpoint in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+    ```ruby
+    registry['notifications'] = [
+      {
+        'name' => 'test_endpoint',
+        'url' => 'https://gitlab.example.com/notify',
+        'timeout' => '500ms',
+        'threshold' => 5,
+        'backoff' => '1s',
+        'headers' => {
+          "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"]
+        }
+      }
+    ]
+    ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the notification endpoint is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+notifications:
+  endpoints:
+    - name: alistener
+      disabled: false
+      url: https://my.listener.com/event
+      headers: <http.Header>
+      timeout: 500
+      threshold: 5
+      backoff: 1000
+```
+
 ## Changelog
 
 **GitLab 8.8 ([source docs][8-8-docs])**
@@ -510,3 +606,5 @@ configurable in future releases.
 [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
 [existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
 [new-domain]: #configure-container-registry-under-its-own-domain
+[notifications-config]: https://docs.docker.com/registry/notifications/
+[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
\ No newline at end of file
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index e4f94eb7cb68d44fbbd04216f94f72ae27f8cee8..0a08591c3ce19ed2346e290283da07dc90e9c08e 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -16,7 +16,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
 1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
    needs privileges to create the `gitlabhq_production` database.
 1. Configure the GitLab application servers with the appropriate details.
-   This step is covered in [Configuring GitLab for HA](gitlab.md)
+   This step is covered in [Configuring GitLab for HA](gitlab.md).
 
 ## Configure using Omnibus
 
@@ -105,6 +105,8 @@ 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. Configure the GitLab application servers with the appropriate details.
+   This step is covered in [Configuring GitLab for HA](gitlab.md).
 
 ---
 
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index dad8e956c0e2826b5e91629fef71ab87dea8f0e2..3245988fc14913bf3cf9087a317f5e22c8f3021c 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -19,8 +19,8 @@ you need to use with GitLab.
 ## GitLab Pages Ports
 
 If you're using GitLab Pages you will need some additional port configurations.
-GitLab Pages requires a separate VIP. Configure DNS to point the
-`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the
+GitLab Pages requires a separate virtual IP address. Configure DNS to point the
+`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
 [GitLab Pages documentation][gitlab-pages] for more information.
 
 | LB Port | Backend Port | Protocol |
@@ -32,7 +32,7 @@ GitLab Pages requires a separate VIP. Configure DNS to point the
 
 Some organizations have policies against opening SSH port 22. In this case,
 it may be helpful to configure an alternate SSH hostname that allows users
-to use SSH on port 443. An alternate SSH hostname will require a new VIP
+to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
 compared to the other GitLab HTTP configuration above.
 
 Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
index 86ef9d167e294dc7bbcff27d05019664af527443..edb9c911aac45ce2f1c1c540f35385aead6dc290 100644
--- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
+++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
@@ -13,7 +13,7 @@ To enable the GitLab monitor exporter:
 1. Add or find and uncomment the following line, making sure it's set to `true`:
 
     ```ruby
-    gitlab_monitor_exporter['enable'] = true
+    gitlab_monitor['enable'] = true
     ```
 
 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 3a394c561db18574d3b566ce4d4e051d982a142e..b2445d1c0e511178742eda4ffdfcac95b43bd222 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -3,10 +3,9 @@
 >**Notes:**
 - Prometheus and the various exporters listed in this page are bundled in the
   Omnibus GitLab package. Check each exporter's documentation for the timeline
-  they got added. For installations from source you will have to install
-  them yourself. Over subsequent releases additional GitLab metrics will be
-  captured.
-- Prometheus services are off by default but will be on starting with GitLab 9.0.
+  they got added. For installations from source you will have to install them
+  yourself. Over subsequent releases additional GitLab metrics will be captured.
+- Prometheus services are on by default with GitLab 9.0.
 - Prometheus and its exporters do not authenticate users, and will be available
   to anyone who can access them.
 
@@ -26,26 +25,25 @@ dashboard tool like [Grafana].
 ## Configuring Prometheus
 
 >**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
+For installations from source you'll have to install and configure it yourself.
 
-To enable Prometheus:
+Prometheus and it's exporters are on by default, starting with GitLab 9.0.
+Prometheus will run as the `gitlab-prometheus` user and listen on
+`http://localhost:9090`. Each exporter will be automatically be set up as a
+monitoring target for Prometheus, unless individually disabled.
+
+To disable Prometheus and all of its exporters, as well as any added in the future:
 
 1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line, making sure it's set to `true`:
+1. Add or find and uncomment the following line, making sure it's set to `false`:
 
     ```ruby
-    prometheus['enable'] = true
+    prometheus_monitoring['enable'] = false
     ```
 
 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
    take effect
 
-By default, Prometheus will run as the `gitlab-prometheus` user and listen on
-`http://localhost:9090`. If the [node exporter](#node-exporter) service
-has been enabled, it will automatically be set up as a monitoring target for
-Prometheus.
-
 ## Changing the port Prometheus listens on
 
 >**Note:**
@@ -71,16 +69,14 @@ To change the address/port that Prometheus listens on:
 
 ## Viewing performance metrics
 
-After you have [enabled Prometheus](#configuring-prometheus), you can visit
-`http://localhost:9090` for the dashboard that Prometheus offers by default.
+You can visit `http://localhost:9090` for the dashboard that Prometheus offers by default.
 
 >**Note:**
 If SSL has been enabled on your GitLab instance, you may not be able to access
 Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
 [provide access via GitLab][multi-user-prometheus], but in the interim there are
 some workarounds: using a separate browser for Prometheus, resetting HSTS, or
-having [Nginx proxy it][nginx-custom-config]. Follow issue [#27069] for more
-information.
+having [Nginx proxy it][nginx-custom-config].
 
 The performance data collected by Prometheus can be viewed directly in the
 Prometheus console or through a compatible dashboard tool.
@@ -96,6 +92,24 @@ Sample Prometheus queries:
 - **Data transmitted:** `irate(node_network_transmit_bytes[5m])`
 - **Data received:** `irate(node_network_receive_bytes[5m])`
 
+## Configuring Prometheus to monitor Kubernetes
+
+> Introduced in GitLab 9.0.
+
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes in the cluster including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
+
+To disable the monitoring of Kubernetes:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line and set it to `false`:
+
+    ```ruby
+    prometheus['monitor_kubernetes'] = false
+    ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+   take effect
+
 ## Prometheus exporters
 
 There are a number of libraries and servers which help in exporting existing
@@ -143,5 +157,6 @@ The GitLab monitor exporter allows you to measure various GitLab metrics.
 [prom-grafana]: https://prometheus.io/docs/visualization/grafana/
 [scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
 [reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
-[#27069]: https://gitlab.com/gitlab-org/gitlab-ce/issues/27069
 [1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
+[prometheus integration]: ../../../user/project/integrations/prometheus.md
+[prometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 8de0cc5af5c8c5c5169ac564d1414c3bc4d42651..0c63b0b59a738c35a07943126f64f1a7486e2892 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -26,22 +26,24 @@ it works.
 
 ---
 
-In the case of custom domains, the Pages daemon needs to listen on ports `80`
-and/or `443`. For that reason, there is some flexibility in the way which you
-can set it up:
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
+which you can set it up:
 
-1. Run the pages daemon in the same server as GitLab, listening on a secondary IP.
-1. Run the pages daemon in a separate server. In that case, the
+1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
+1. Run the Pages daemon in a separate server. In that case, the
    [Pages path](#change-storage-path) must also be present in the server that
-   the pages daemon is installed, so you will have to share it via network.
-1. Run the pages daemon in the same server as GitLab, listening on the same IP
+   the Pages daemon is installed, so you will have to share it via network.
+1. Run the Pages daemon in the same server as GitLab, listening on the same IP
    but on different ports. In that case, you will have to proxy the traffic with
    a loadbalancer. If you choose that route note that you should use TCP load
    balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
    pages will not be able to be served with user provided certificates. For
    HTTP it's OK to use HTTP or TCP load balancing.
 
-In this document, we will proceed assuming the first option.
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
 
 ## Prerequisites
 
@@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to:
    serve Pages under HTTPS.
 1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
    so that your users don't have to bring their own.
+1. (Only for custom domains) Have a **secondary IP**.
 
 ### DNS configuration
 
@@ -62,11 +65,13 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
 host that GitLab runs. For example, an entry would look like this:
 
 ```
-*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN A    1.1.1.1
+*.example.io. 1800 IN AAAA 2001::1
 ```
 
 where `example.io` is the domain under which GitLab Pages will be served
-and `1.1.1.1` is the IP address of your GitLab instance.
+and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the
+IPv6 address. If you don't have IPv6, you can omit the AAAA record.
 
 > **Note:**
 You should not use the GitLab domain to serve user pages. For more information
@@ -102,6 +107,8 @@ The Pages daemon doesn't listen to the outside world.
 
 1. [Reconfigure GitLab][reconfigure]
 
+Watch the [video tutorial][video-admin] for this configuration.
+
 ### Wildcard domains with TLS support
 
 >**Requirements:**
@@ -136,7 +143,8 @@ outside world.
 In addition to the wildcard domains, you can also have the option to configure
 GitLab Pages to work with custom domains. Again, there are two options here:
 support custom domains with and without TLS certificates. The easiest setup is
-that without TLS certificates.
+that without TLS certificates. In either case, you'll need a secondary IP. If
+you have IPv6 as well as IPv4 addresses, you can use them both.
 
 ### Custom domains
 
@@ -148,7 +156,7 @@ that without TLS certificates.
 >
 URL scheme: `http://page.example.io` and `http://domain.com`
 
-In that case, the pages daemon is running, Nginx still proxies requests to
+In that case, the Pages daemon is running, Nginx still proxies requests to
 the daemon but the daemon is also able to receive requests from the outside
 world. Custom domains are supported, but no TLS.
 
@@ -158,11 +166,12 @@ world. Custom domains are supported, but no TLS.
     pages_external_url "http://example.io"
     nginx['listen_addresses'] = ['1.1.1.1']
     pages_nginx['enable'] = false
-    gitlab_pages['external_http'] = '1.1.1.2:80'
+    gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
     ```
 
     where `1.1.1.1` is the primary IP address that GitLab is listening to and
-    `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+    `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
+    listens on. If you don't have IPv6, you can omit the IPv6 address.
 
 1. [Reconfigure GitLab][reconfigure]
 
@@ -177,7 +186,7 @@ world. Custom domains are supported, but no TLS.
 >
 URL scheme: `https://page.example.io` and `https://domain.com`
 
-In that case, the pages daemon is running, Nginx still proxies requests to
+In that case, the Pages daemon is running, Nginx still proxies requests to
 the daemon but the daemon is also able to receive requests from the outside
 world. Custom domains and TLS are supported.
 
@@ -189,12 +198,13 @@ world. Custom domains and TLS are supported.
     pages_nginx['enable'] = false
     gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
     gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
-    gitlab_pages['external_http'] = '1.1.1.2:80'
-    gitlab_pages['external_https'] = '1.1.1.2:443'
+    gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
+    gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443']
     ```
 
     where `1.1.1.1` is the primary IP address that GitLab is listening to and
-    `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+    `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
+    listens on. If you don't have IPv6, you can omit the IPv6 address.
 
 1. [Reconfigure GitLab][reconfigure]
 
@@ -270,3 +280,4 @@ latest previous version.
 [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
 [restart]: ../restart_gitlab.md#installations-from-source
 [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[video-admin]: https://youtu.be/dD8c7WNcc6s
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 463715e48cab77080dda461ea52c6b72dd8fad7a..a45c330645785eb15c65e3c80f2d63a65761cf04 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -1,5 +1,9 @@
 # GitLab Pages administration for source installations
 
+>**Note:**
+Before attempting to enable GitLab Pages, first make sure you have
+[installed GitLab](../../install/installation.md) successfully.
+
 This is the documentation for configuring a GitLab Pages when you have installed
 GitLab from source and not using the Omnibus packages.
 
@@ -13,18 +17,47 @@ Pages to the latest supported version.
 
 ## Overview
 
-[Read the Omnibus overview section.](index.md#overview)
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
 
-## Prerequisites
+---
 
-Before proceeding with the Pages configuration, you will need to:
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
+which you can set it up:
+
+1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
+1. Run the Pages daemon in a separate server. In that case, the
+   [Pages path](#change-storage-path) must also be present in the server that
+   the Pages daemon is installed, so you will have to share it via network.
+1. Run the Pages daemon in the same server as GitLab, listening on the same IP
+   but on different ports. In that case, you will have to proxy the traffic with
+   a loadbalancer. If you choose that route note that you should use TCP load
+   balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+   pages will not be able to be served with user provided certificates. For
+   HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
 
-1. Have a separate domain under which the GitLab Pages will be served. In this
-   document we assume that to be `example.io`.
-1. Configure a **wildcard DNS record**.
-1. (Optional) Have a **wildcard certificate** for that domain if you decide to
-   serve Pages under HTTPS.
-1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
+## Prerequisites
+
+Before proceeding with the Pages configuration, make sure that:
+
+1. You have a separate domain under which GitLab Pages will be served. In
+   this document we assume that to be `example.io`.
+1. You have configured a **wildcard DNS record** for that domain.
+1. You have installed the `zip` and `unzip` packages in the same server that
+   GitLab is installed since they are needed to compress/uncompress the
+   Pages artifacts.
+1. (Optional) You have a **wildcard certificate** for the Pages domain if you
+   decide to serve Pages (`*.example.io`) under HTTPS.
+1. (Optional but recommended) You have configured and enabled the [Shared Runners][]
    so that your users don't have to bring their own.
 
 ### DNS configuration
@@ -72,7 +105,7 @@ The Pages daemon doesn't listen to the outside world.
     cd /home/git
     sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
     cd gitlab-pages
-    sudo -u git -H git checkout v0.2.4
+    sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
     sudo -u git -H make
     ```
 
@@ -97,14 +130,21 @@ The Pages daemon doesn't listen to the outside world.
        https: false
      ```
 
-1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+   order to enable the pages daemon. In `gitlab_pages_options` the
+   `-pages-domain` must match the `host` setting that you set above.
 
-    ```bash
-    sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
-    sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+    ```
+    gitlab_pages_enabled=true
+    gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090
     ```
 
-      Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+1. Copy the `gitlab-pages` Nginx configuration file:
+
+    ```bash
+    sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+    sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
+    ```
 
 1. Restart NGINX
 1. [Restart GitLab][restart]
@@ -128,7 +168,7 @@ outside world.
     cd /home/git
     sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
     cd gitlab-pages
-    sudo -u git -H git checkout v0.2.4
+    sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
     sudo -u git -H make
     ```
 
@@ -146,6 +186,17 @@ outside world.
        https: true
      ```
 
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+   order to enable the pages daemon. In `gitlab_pages_options` the
+   `-pages-domain` must match the `host` setting that you set above.
+   The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+   of the `example.io` domain:
+
+    ```
+    gitlab_pages_enabled=true
+    gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+    ```
+
 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
 
     ```bash
@@ -153,12 +204,9 @@ outside world.
     sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
     ```
 
-      Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
 1. Restart NGINX
 1. [Restart GitLab][restart]
 
-
 ## Advanced configuration
 
 In addition to the wildcard domains, you can also have the option to configure
@@ -186,7 +234,7 @@ world. Custom domains are supported, but no TLS.
     cd /home/git
     sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
     cd gitlab-pages
-    sudo -u git -H git checkout v0.2.4
+    sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
     sudo -u git -H make
     ```
 
@@ -221,12 +269,10 @@ world. Custom domains are supported, but no TLS.
 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
 
     ```bash
-    sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
-    sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+    sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+    sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
     ```
 
-      Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
    `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
    listens to.
@@ -254,7 +300,7 @@ world. Custom domains and TLS are supported.
     cd /home/git
     sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
     cd gitlab-pages
-    sudo -u git -H git checkout v0.2.4
+    sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
     sudo -u git -H make
     ```
 
@@ -297,8 +343,6 @@ world. Custom domains and TLS are supported.
     sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
     ```
 
-      Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
    `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
    listens to.
@@ -389,4 +433,6 @@ than GitLab to prevent XSS attacks.
 [pages-userguide]: ../../user/project/pages/index.md
 [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
 [restart]: ../restart_gitlab.md#installations-from-source
-[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.4.0
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example
+[shared runners]: ../../ci/runners/README.md
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 00494e7e9d6823962fb034f7328666583bc7e4f1..e99a7ee29cc7d0d898151a13053ee5163fb4ea80 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -13,7 +13,8 @@ three strategies for this feature:
 
 ### Email sub-addressing
 
-**If your provider or server supports email sub-addressing, we recommend using it.**
+**If your provider or server supports email sub-addressing, we recommend using it.
+Some features (e.g. create new issue via email) only work with sub-addressing.**
 
 [Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
 a feature where any email to `user+some_arbitrary_tag@example.com` will end up
@@ -69,7 +70,9 @@ please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
 
 If you want to use Gmail / Google Apps with Reply by email, make sure you have
 [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
-and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
+or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
+and use [an application password](https://support.google.com/mail/answer/185833).
 
 To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
 [Postfix setup documentation](reply_by_email_postfix_setup.md).
@@ -138,12 +141,32 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
     # The IDLE command timeout.
     gitlab_rails['incoming_email_idle_timeout'] = 60
     ```
+    
+    ```ruby
+    # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
+    gitlab_rails['incoming_email_enabled'] = true
+
+    # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+    gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
+
+    # Email account username
+    # Typically this is the userPrincipalName (UPN)
+    gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com"
+    # Email account password
+    gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+    # IMAP server host
+    gitlab_rails['incoming_email_host'] = "exchange.example.com"
+    # IMAP server port
+    gitlab_rails['incoming_email_port'] = 993
+    # Whether the IMAP server uses SSL
+    gitlab_rails['incoming_email_ssl'] = true
+    ```
 
-1. Reconfigure GitLab and restart mailroom for the changes to take effect:
+1. Reconfigure GitLab for the changes to take effect:
 
     ```sh
     sudo gitlab-ctl reconfigure
-    sudo gitlab-ctl restart mailroom
     ```
 
 1. Verify that everything is configured correctly:
@@ -230,6 +253,35 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
       # The IDLE command timeout.
       idle_timeout: 60
     ```
+    
+    ```yaml
+    # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
+    incoming_email:
+      enabled: true
+
+      # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+      address: "incoming@exchange.example.com"
+
+      # Email account username
+      # Typically this is the userPrincipalName (UPN)
+      user: "incoming@ad-domain.example.com"
+      # Email account password
+      password: "[REDACTED]"
+
+      # IMAP server host
+      host: "exchange.example.com"
+      # IMAP server port
+      port: 993
+      # Whether the IMAP server uses SSL
+      ssl: true
+      # Whether the IMAP server uses StartTLS
+      start_tls: false
+
+      # The mailbox where incoming mail will end up. Usually "inbox".
+      mailbox: "inbox"
+      # The IDLE command timeout.
+      idle_timeout: 60
+    ```
 
 1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
 
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index d6aa610102627497e70787098be5974274968f17..55a451195250d5e112e856669262cfce72d0ccdd 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -52,9 +52,12 @@ respectively.
       # Paths where repositories can be stored. Give the canonicalized absolute pathname.
       # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
       storages: # You must have at least a 'default' storage path.
-        default: /home/git/repositories
-        nfs: /mnt/nfs/repositories
-        cephfs: /mnt/cephfs/repositories
+        default:
+          path: /home/git/repositories
+        nfs:
+          path: /mnt/nfs/repositories
+        cephfs:
+          path: /mnt/cephfs/repositories
     ```
 
 1. [Restart GitLab] for the changes to take effect.
@@ -75,9 +78,9 @@ working, you can remove the `repos_path` line.
 
     ```ruby
     git_data_dirs({
-      "default" => "/var/opt/gitlab/git-data",
-      "nfs" => "/mnt/nfs/git-data",
-      "cephfs" => "/mnt/cephfs/git-data"
+      "default" => { "path" => "/var/opt/gitlab/git-data" },
+      "nfs" => { "path" => "/mnt/nfs/git-data" },
+      "cephfs" => { "path" => "/mnt/cephfs/git-data" }
     })
     ```
 
diff --git a/doc/api/README.md b/doc/api/README.md
index b334ca46caf926b1fe74f392386b93424f8b715d..58d090b8f5e7df302a0b9765653dbc3bed8ba8ae 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -11,8 +11,6 @@ following locations:
 - [Award Emoji](award_emoji.md)
 - [Branches](branches.md)
 - [Broadcast Messages](broadcast_messages.md)
-- [Builds](builds.md)
-- [Build Triggers](build_triggers.md)
 - [Build Variables](build_variables.md)
 - [Commits](commits.md)
 - [Deployments](deployments.md)
@@ -24,6 +22,7 @@ following locations:
 - [Group Members](members.md)
 - [Issues](issues.md)
 - [Issue Boards](boards.md)
+- [Jobs](jobs.md)
 - [Keys](keys.md)
 - [Labels](labels.md)
 - [Merge Requests](merge_requests.md)
@@ -33,6 +32,7 @@ following locations:
 - [Notes](notes.md) (comments)
 - [Notification settings](notification_settings.md)
 - [Pipelines](pipelines.md)
+- [Pipeline Triggers](pipeline_triggers.md)
 - [Projects](projects.md) including setting Webhooks
 - [Project Access Requests](access_requests.md)
 - [Project Members](members.md)
@@ -89,7 +89,7 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i
 Example of using the OAuth2 token in the header:
 
 ```shell
-curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
 ```
 
 Read more about [GitLab as an OAuth2 client](oauth2.md).
@@ -127,13 +127,13 @@ is defined in [`lib/api.rb`][lib-api-url].
 Example of a valid API request:
 
 ```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
+GET https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
 ```
 
 Example of a valid API request using cURL and authentication via header:
 
 ```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
 ```
 
 The API uses JSON to serialize data. You don't need to specify `.json` at the
@@ -159,6 +159,7 @@ The following table shows the possible return codes for API requests.
 | Return values | Description |
 | ------------- | ----------- |
 | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
+| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
 | `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
 | `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
 | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
@@ -206,7 +207,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
 ```
 
 ```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects"
 ```
 
 Example of a valid API call and a request using cURL with sudo request,
@@ -217,9 +218,17 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
 ```
 
 ```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
 ```
 
+## Impersonation Tokens
+
+Impersonation Tokens are a type of Personal Access Token that can only be created by an admin for a specific user. These can be used by automated tools
+to authenticate with the API as a specific user, as a better alternative to using the user's password or private token directly, which may change over time,
+and to using the [Sudo](#sudo) feature, which requires the tool to know an admin's password or private token, which can change over time as well and are extremely powerful.
+
+For more information about the usage please refer to the [Users](users.md) page
+
 ## Pagination
 
 Sometimes the returned result will span across many pages. When listing
@@ -233,7 +242,7 @@ resources you can pass the following parameters:
 In the example below, we list 50 [namespaces](namespaces.md) per page.
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/namespaces?per_page=50
 ```
 
 ### Pagination Link header
@@ -247,7 +256,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue
 with ID `8` which belongs to the project with ID `8`:
 
 ```bash
-curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
+curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/issues/8/notes?per_page=3&page=2
 ```
 
 The response will then be:
@@ -258,7 +267,7 @@ Cache-Control: no-cache
 Content-Length: 1103
 Content-Type: application/json
 Date: Mon, 18 Jan 2016 09:43:18 GMT
-Link: <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
+Link: <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
 Status: 200 OK
 Vary: Origin
 X-Next-Page: 3
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index dee3e3840807477e904ac640c1e140c0db87de3b..96b8d654c58a592d80ecfafa9b4a5b197adb2967 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -28,8 +28,8 @@ GET /projects/:id/access_requests
 | `id`      | integer/string | yes | The group/project ID or path |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests
 ```
 
 Example response:
@@ -69,8 +69,8 @@ POST /projects/:id/access_requests
 | `id`      | integer/string | yes | The group/project ID or path |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests
 ```
 
 Example response:
@@ -102,8 +102,8 @@ PUT /projects/:id/access_requests/:user_id/approve
 | `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id/approve?access_level=20
 ```
 
 Example response:
@@ -134,6 +134,6 @@ DELETE /projects/:id/access_requests/:user_id
 | `user_id` | integer | yes   | The user ID of the access requester |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id
 ```
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 58092bdd400890e3102f7c24fb45daea2691fb34..f57928d3c9320904a26cd611d249dd658bff9f4a 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -14,20 +14,20 @@ requests, snippets, and notes/comments. Issues, merge requests, snippets, and no
 Gets a list of all award emoji
 
 ```
-GET /projects/:id/issues/:issue_id/award_emoji
-GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+GET /projects/:id/issues/:issue_iid/award_emoji
+GET /projects/:id/merge_requests/:merge_request_iid/award_emoji
 GET /projects/:id/snippets/:snippet_id/award_emoji
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
+| Attribute      | Type    | Required | Description                                                                 |
+| ---------      | ----    | -------- | -----------                                                                 |
+| `id`           | integer | yes      | The ID of a project                                                         |
+| `awardable_id` | integer | yes      | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji
 ```
 
 Example Response:
@@ -74,21 +74,21 @@ Example Response:
 Gets a single award emoji from an issue, snippet, or merge request.
 
 ```
-GET /projects/:id/issues/:issue_id/award_emoji/:award_id
-GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+GET /projects/:id/issues/:issue_iid/award_emoji/:award_id
+GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
 GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
-| `award_id` | integer | yes | The ID of the award emoji |
+| Attribute      | Type    | Required | Description                                                                 |
+| ---------      | ----    | -------- | -----------                                                                 |
+| `id`           | integer | yes      | The ID of a project                                                         |
+| `awardable_id` | integer | yes      | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
+| `award_id`     | integer | yes      | The ID of the award emoji                                                   |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1
 ```
 
 Example Response:
@@ -117,21 +117,21 @@ Example Response:
 This end point creates an award emoji on the specified resource
 
 ```
-POST /projects/:id/issues/:issue_id/award_emoji
-POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+POST /projects/:id/issues/:issue_iid/award_emoji
+POST /projects/:id/merge_requests/:merge_request_iid/award_emoji
 POST /projects/:id/snippets/:snippet_id/award_emoji
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
-| `name` | string | yes | The name of the emoji, without colons |
+| Attribute      | Type    | Required | Description                                                                 |
+| ---------      | ----    | -------- | -----------                                                                 |
+| `id`           | integer | yes      | The ID of a project                                                         |
+| `awardable_id` | integer | yes      | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
+| `name`         | string  | yes      | The name of the emoji, without colons                                       |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish
 ```
 
 Example Response:
@@ -161,42 +161,21 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a
 admins or the author of the award.
 
 ```
-DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
-DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id
+DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
 DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `award_id` | integer | yes | The ID of a award_emoji |
+| Attribute   | Type    | Required | Description                 |
+| ---------   | ----    | -------- | -----------                 |
+| `id`        | integer | yes      | The ID of a project         |
+| `issue_iid` | integer | yes      | The internal ID of an issue |
+| `award_id`  | integer | yes      | The ID of a award_emoji     |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
-```
-
-Example Response:
-
-```json
-{
-  "id": 344,
-  "name": "blowfish",
-  "user": {
-    "name": "Administrator",
-    "username": "root",
-    "id": 1,
-    "state": "active",
-    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/root"
-  },
-  "created_at": "2016-06-17T17:47:29.266Z",
-  "updated_at": "2016-06-17T17:47:29.266Z",
-  "awardable_id": 80,
-  "awardable_type": "Issue"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344
 ```
 
 ## Award Emoji on Notes
@@ -209,20 +188,20 @@ easily adapted for notes on a Merge Request.
 ### List a note's award emoji
 
 ```
-GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of an note |
+| Attribute   | Type    | Required | Description                 |
+| ---------   | ----    | -------- | -----------                 |
+| `id`        | integer | yes      | The ID of a project         |
+| `issue_iid` | integer | yes      | The internal ID of an issue |
+| `note_id`   | integer | yes      | The ID of an note           |
 
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji
 ```
 
 Example Response:
@@ -251,20 +230,20 @@ Example Response:
 ### Get single note's award emoji
 
 ```
-GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `award_id` | integer | yes | The ID of the award emoji |
+| Attribute   | Type    | Required | Description                 |
+| ---------   | ----    | -------- | -----------                 |
+| `id`        | integer | yes      | The ID of a project         |
+| `issue_iid` | integer | yes      | The internal ID of an issue |
+| `note_id`   | integer | yes      | The ID of a note            |
+| `award_id`  | integer | yes      | The ID of the award emoji   |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2
 ```
 
 Example Response:
@@ -291,20 +270,20 @@ Example Response:
 ### Award a new emoji on a note
 
 ```
-POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `name` | string | yes | The name of the emoji, without colons |
+| Attribute   | Type    | Required | Description                           |
+| ---------   | ----    | -------- | -----------                           |
+| `id`        | integer | yes      | The ID of a project                   |
+| `issue_iid` | integer | yes      | The internal ID of an issue           |
+| `note_id`   | integer | yes      | The ID of a note                      |
+| `name`      | string  | yes      | The name of the emoji, without colons |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket
 ```
 
 Example Response:
@@ -334,41 +313,20 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a
 admins or the author of the award.
 
 ```
-DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
 ```
 
 Parameters:
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `award_id` | integer | yes | The ID of a award_emoji |
+| Attribute   | Type    | Required | Description                 |
+| ---------   | ----    | -------- | -----------                 |
+| `id`        | integer | yes      | The ID of a project         |
+| `issue_iid` | integer | yes      | The internal ID of an issue |
+| `note_id`   | integer | yes      | The ID of a note            |
+| `award_id`  | integer | yes      | The ID of a award_emoji     |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
-```
-
-Example Response:
-
-```json
-{
-  "id": 345,
-  "name": "rocket",
-  "user": {
-    "name": "Administrator",
-    "username": "root",
-    "id": 1,
-    "state": "active",
-    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/root"
-  },
-  "created_at": "2016-06-17T19:59:55.888Z",
-  "updated_at": "2016-06-17T19:59:55.888Z",
-  "awardable_id": 1,
-  "awardable_type": "Note"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345
 ```
 
 [ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575
diff --git a/doc/api/boards.md b/doc/api/boards.md
index c83db6df80cd3440b36f1886d1e82db1304e38e8..a74e82335eb5561aa75f261bc22344a892b22c28 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -18,7 +18,7 @@ GET /projects/:id/boards
 | `id`   | integer  | yes    | The ID of a project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
 ```
 
 Example response:
@@ -75,7 +75,7 @@ GET /projects/:id/boards/:board_id/lists
 | `board_id`   | integer  | yes    | The ID of a board |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
 ```
 
 Example response:
@@ -127,7 +127,7 @@ GET /projects/:id/boards/:board_id/lists/:list_id
 | `list_id`| integer | yes   | The ID of a board's list |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
 ```
 
 Example response:
@@ -159,7 +159,7 @@ POST /projects/:id/boards/:board_id/lists
 | `label_id`         | integer  | yes | The ID of a label |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
 ```
 
 Example response:
@@ -192,7 +192,7 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
 | `position`         | integer  | yes  | The position of the list |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
 ```
 
 Example response:
@@ -224,18 +224,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
 | `list_id`      | integer | yes | The ID of a board's list |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
-```
-Example response:
-
-```json
-{
-  "id" : 1,
-  "label" : {
-    "name" : "Testing",
-    "color" : "#F0AD4E",
-    "description" : null
-  },
-  "position" : 1
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
 ```
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 765ca439720e36489ba7745a0a7b11af22a768a2..815aabda8e3fdabead4a76363d68a3b52babc9ae 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -3,6 +3,8 @@
 ## List repository branches
 
 Get a list of repository branches from a project, sorted by name alphabetically.
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
 
 ```
 GET /projects/:id/repository/branches
@@ -13,7 +15,7 @@ GET /projects/:id/repository/branches
 | `id` | integer | yes | The ID of a project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches
 ```
 
 Example response:
@@ -48,7 +50,8 @@ Example response:
 
 ## Get single repository branch
 
-Get a single project repository branch.
+Get a single project repository branch. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
 
 ```
 GET /projects/:id/repository/branches/:branch
@@ -60,7 +63,7 @@ GET /projects/:id/repository/branches/:branch
 | `branch` | string | yes | The name of the branch |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master
 ```
 
 Example response:
@@ -101,7 +104,7 @@ PUT /projects/:id/repository/branches/:branch/protect
 ```
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
 ```
 
 | Attribute | Type | Required | Description |
@@ -149,7 +152,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect
 ```
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect
 ```
 
 | Attribute | Type | Required | Description |
@@ -197,7 +200,7 @@ POST /projects/:id/repository/branches
 | `ref`         | string  | yes | The branch name or commit SHA to create branch from |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch=newbranch&ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master"
 ```
 
 Example response:
@@ -241,15 +244,7 @@ DELETE /projects/:id/repository/branches/:branch
 In case of an error, an explaining message is provided.
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
-```
-
-Example response:
-
-```json
-{
-  "branch_name": "newbranch"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch"
 ```
 
 ## Delete merged branches
@@ -266,5 +261,5 @@ DELETE /projects/:id/repository/merged_branches
 
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches"
 ```
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index a3e9c01f335e9dde73a8d5287d9d992c84578bd9..ad254e3515ee257ffbbc03a6c9ffe4d306d0ac94 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -13,7 +13,7 @@ GET /broadcast_messages
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages
 ```
 
 Example response:
@@ -43,7 +43,7 @@ GET /broadcast_messages/:id
 | `id`        | integer  | yes      | Broadcast message ID      |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
 ```
 
 Example response:
@@ -75,7 +75,7 @@ POST /broadcast_messages
 | `font`      | string   | no       | Foreground color hex code                            |
 
 ```bash
-curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages
 ```
 
 Example response:
@@ -108,7 +108,7 @@ PUT /broadcast_messages/:id
 | `font`      | string   | no       | Foreground color hex code |
 
 ```bash
-curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
 ```
 
 Example response:
@@ -136,19 +136,5 @@ DELETE /broadcast_messages/:id
 | `id`        | integer  | yes      | Broadcast message ID      |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
-```
-
-Example response:
-
-```json
-{
-    "message":"Update message",
-    "starts_at":"2016-08-26T00:41:35.060Z",
-    "ends_at":"2016-08-26T01:41:35.060Z",
-    "color":"#000",
-    "font":"#FFFFFF",
-    "id":1,
-    "active": true
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
 ```
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index b6459971420fc1341d22eec7aeb78ed5d5d58951..20d924ab35e1f119eec43b06660347ef01e69f06 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -1,118 +1 @@
-# Build triggers
-
-You can read more about [triggering builds through the API](../ci/triggers/README.md).
-
-## List project triggers
-
-Get a list of project's build triggers.
-
-```
-GET /projects/:id/triggers
-```
-
-| Attribute | Type    | required | Description         |
-|-----------|---------|----------|---------------------|
-| `id`      | integer | yes      | The ID of a project |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
-```
-
-```json
-[
-    {
-        "created_at": "2015-12-23T16:24:34.716Z",
-        "deleted_at": null,
-        "last_used": "2016-01-04T15:41:21.986Z",
-        "token": "fbdb730c2fbdb095a0862dbd8ab88b",
-        "updated_at": "2015-12-23T16:24:34.716Z"
-    },
-    {
-        "created_at": "2015-12-23T16:25:56.760Z",
-        "deleted_at": null,
-        "last_used": null,
-        "token": "7b9148c158980bbd9bcea92c17522d",
-        "updated_at": "2015-12-23T16:25:56.760Z"
-    }
-]
-```
-
-## Get trigger details
-
-Get details of project's build trigger.
-
-```
-GET /projects/:id/triggers/:token
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-| `token`   | string  | yes      | The `token` of a trigger |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
-
-```json
-{
-    "created_at": "2015-12-23T16:25:56.760Z",
-    "deleted_at": null,
-    "last_used": null,
-    "token": "7b9148c158980bbd9bcea92c17522d",
-    "updated_at": "2015-12-23T16:25:56.760Z"
-}
-```
-
-## Create a project trigger
-
-Create a build trigger for a project.
-
-```
-POST /projects/:id/triggers
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
-```
-
-```json
-{
-    "created_at": "2016-01-07T09:53:58.235Z",
-    "deleted_at": null,
-    "last_used": null,
-    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
-    "updated_at": "2016-01-07T09:53:58.235Z"
-}
-```
-
-## Remove a project trigger
-
-Remove a project's build trigger.
-
-```
-DELETE /projects/:id/triggers/:token
-```
-
-| Attribute | Type    | required | Description              |
-|-----------|---------|----------|--------------------------|
-| `id`      | integer | yes      | The ID of a project      |
-| `token`   | string  | yes      | The `token` of a trigger |
-
-```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
-
-```json
-{
-    "created_at": "2015-12-23T16:25:56.760Z",
-    "deleted_at": "2015-12-24T12:32:20.100Z",
-    "last_used": null,
-    "token": "7b9148c158980bbd9bcea92c17522d",
-    "updated_at": "2015-12-24T12:32:20.100Z"
-}
-```
+This document was moved to [Pipeline Triggers](pipeline_triggers.md).
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 917e9773913077527da6210c7d813c860d0a26eb..1c26e9b33ab441f4ae5458ea647f324f66440133 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -13,7 +13,7 @@ GET /projects/:id/variables
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables"
 ```
 
 ```json
@@ -43,7 +43,7 @@ GET /projects/:id/variables/:key
 | `key`     | string  | yes      | The `key` of a variable |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/TEST_VARIABLE_1"
 ```
 
 ```json
@@ -68,7 +68,7 @@ POST /projects/:id/variables
 | `value`   | string  | yes      | The `value` of a variable |
 
 ```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
 ```
 
 ```json
@@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key
 | `value`   | string  | yes      | The `value` of a variable |
 
 ```
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
 ```
 
 ```json
@@ -117,12 +117,5 @@ DELETE /projects/:id/variables/:key
 | `key`     | string  | yes      | The `key` of a variable |
 
 ```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
-```
-
-```json
-{
-    "key": "VARIABLE_1",
-    "value": "VALUE_1"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/VARIABLE_1"
 ```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index bca2f9e44ef94319ce8d5c35bd81993e70383063..a6edda68bc4c9aef2bc34e6cffe2f579ebc9d49b 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -1,610 +1 @@
-# Builds API
-
-## List project builds
-
-Get a list of builds in a project.
-
-```
-GET /projects/:id/builds
-```
-
-| Attribute | Type    | Required | Description         |
-|-----------|---------|----------|---------------------|
-| `id`      | integer | yes      | The ID of a project |
-| `scope`   | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
-```
-
-Example of response
-
-```json
-[
-  {
-    "commit": {
-      "author_email": "admin@example.com",
-      "author_name": "Administrator",
-      "created_at": "2015-12-24T16:51:14.000+01:00",
-      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "message": "Test the CI integration.",
-      "short_id": "0ff3ae19",
-      "title": "Test the CI integration."
-    },
-    "coverage": null,
-    "created_at": "2015-12-24T15:51:21.802Z",
-    "artifacts_file": {
-      "filename": "artifacts.zip",
-      "size": 1000
-    },
-    "finished_at": "2015-12-24T17:54:27.895Z",
-    "id": 7,
-    "name": "teaspoon",
-    "pipeline": {
-      "id": 6,
-      "ref": "master",
-      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "status": "pending"
-    },
-    "ref": "master",
-    "runner": null,
-    "stage": "test",
-    "started_at": "2015-12-24T17:54:27.722Z",
-    "status": "failed",
-    "tag": false,
-    "user": {
-      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "bio": null,
-      "created_at": "2015-12-21T13:14:24.077Z",
-      "id": 1,
-      "is_admin": true,
-      "linkedin": "",
-      "name": "Administrator",
-      "skype": "",
-      "state": "active",
-      "twitter": "",
-      "username": "root",
-      "web_url": "http://gitlab.dev/root",
-      "website_url": ""
-    }
-  },
-  {
-    "commit": {
-      "author_email": "admin@example.com",
-      "author_name": "Administrator",
-      "created_at": "2015-12-24T16:51:14.000+01:00",
-      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "message": "Test the CI integration.",
-      "short_id": "0ff3ae19",
-      "title": "Test the CI integration."
-    },
-    "coverage": null,
-    "created_at": "2015-12-24T15:51:21.727Z",
-    "artifacts_file": null,
-    "finished_at": "2015-12-24T17:54:24.921Z",
-    "id": 6,
-    "name": "spinach:other",
-    "pipeline": {
-      "id": 6,
-      "ref": "master",
-      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "status": "pending"
-    },
-    "ref": "master",
-    "runner": null,
-    "stage": "test",
-    "started_at": "2015-12-24T17:54:24.729Z",
-    "status": "failed",
-    "tag": false,
-    "user": {
-      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "bio": null,
-      "created_at": "2015-12-21T13:14:24.077Z",
-      "id": 1,
-      "is_admin": true,
-      "linkedin": "",
-      "name": "Administrator",
-      "skype": "",
-      "state": "active",
-      "twitter": "",
-      "username": "root",
-      "web_url": "http://gitlab.dev/root",
-      "website_url": ""
-    }
-  }
-]
-```
-
-## List commit builds
-
-Get a list of builds for specific commit in a project.
-
-This endpoint will return all builds, from all pipelines for a given commit.
-If the commit SHA is not found, it will respond with 404, otherwise it will
-return an array of builds (an empty array if there are no builds for this
-particular commit).
-
-```
-GET /projects/:id/repository/commits/:sha/builds
-```
-
-| Attribute | Type    | Required | Description         |
-|-----------|---------|----------|---------------------|
-| `id`      | integer | yes      | The ID of a project |
-| `sha`     | string  | yes      | The SHA id of a commit |
-| `scope`   | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
-```
-
-Example of response
-
-```json
-[
-  {
-    "commit": {
-      "author_email": "admin@example.com",
-      "author_name": "Administrator",
-      "created_at": "2015-12-24T16:51:14.000+01:00",
-      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "message": "Test the CI integration.",
-      "short_id": "0ff3ae19",
-      "title": "Test the CI integration."
-    },
-    "coverage": null,
-    "created_at": "2016-01-11T10:13:33.506Z",
-    "artifacts_file": null,
-    "finished_at": "2016-01-11T10:14:09.526Z",
-    "id": 69,
-    "name": "rubocop",
-    "pipeline": {
-      "id": 6,
-      "ref": "master",
-      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "status": "pending"
-    },
-    "ref": "master",
-    "runner": null,
-    "stage": "test",
-    "started_at": null,
-    "status": "canceled",
-    "tag": false,
-    "user": null
-  },
-  {
-    "commit": {
-      "author_email": "admin@example.com",
-      "author_name": "Administrator",
-      "created_at": "2015-12-24T16:51:14.000+01:00",
-      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "message": "Test the CI integration.",
-      "short_id": "0ff3ae19",
-      "title": "Test the CI integration."
-    },
-    "coverage": null,
-    "created_at": "2015-12-24T15:51:21.957Z",
-    "artifacts_file": null,
-    "finished_at": "2015-12-24T17:54:33.913Z",
-    "id": 9,
-    "name": "brakeman",
-    "pipeline": {
-      "id": 6,
-      "ref": "master",
-      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-      "status": "pending"
-    },
-    "ref": "master",
-    "runner": null,
-    "stage": "test",
-    "started_at": "2015-12-24T17:54:33.727Z",
-    "status": "failed",
-    "tag": false,
-    "user": {
-      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "bio": null,
-      "created_at": "2015-12-21T13:14:24.077Z",
-      "id": 1,
-      "is_admin": true,
-      "linkedin": "",
-      "name": "Administrator",
-      "skype": "",
-      "state": "active",
-      "twitter": "",
-      "username": "root",
-      "web_url": "http://gitlab.dev/root",
-      "website_url": ""
-    }
-  }
-]
-```
-
-## Get a single build
-
-Get a single build of a project
-
-```
-GET /projects/:id/builds/:build_id
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| `id`       | integer | yes      | The ID of a project |
-| `build_id` | integer | yes      | The ID of a build   |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
-```
-
-Example of response
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "created_at": "2015-12-24T15:51:21.880Z",
-  "artifacts_file": null,
-  "finished_at": "2015-12-24T17:54:31.198Z",
-  "id": 8,
-  "name": "rubocop",
-  "pipeline": {
-    "id": 6,
-    "ref": "master",
-    "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "status": "pending"
-  },
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "started_at": "2015-12-24T17:54:30.733Z",
-  "status": "failed",
-  "tag": false,
-  "user": {
-    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "bio": null,
-    "created_at": "2015-12-21T13:14:24.077Z",
-    "id": 1,
-    "is_admin": true,
-    "linkedin": "",
-    "name": "Administrator",
-    "skype": "",
-    "state": "active",
-    "twitter": "",
-    "username": "root",
-    "web_url": "http://gitlab.dev/root",
-    "website_url": ""
-  }
-}
-```
-
-## Get build artifacts
-
-> [Introduced][ce-2893] in GitLab 8.5
-
-Get build artifacts of a project
-
-```
-GET /projects/:id/builds/:build_id/artifacts
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| `id`       | integer | yes      | The ID of a project |
-| `build_id` | integer | yes      | The ID of a build   |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
-```
-
-Response:
-
-| Status    | Description                     |
-|-----------|---------------------------------|
-| 200       | Serves the artifacts file       |
-| 404       | Build not found or no artifacts |
-
-[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
-
-## Download the artifacts file
-
-> [Introduced][ce-5347] in GitLab 8.10.
-
-Download the artifacts file from the given reference name and job provided the
-build finished successfully.
-
-```
-GET /projects/:id/builds/artifacts/:ref_name/download?job=name
-```
-
-Parameters
-
-| Attribute   | Type    | Required | Description               |
-|-------------|---------|----------|-------------------------- |
-| `id`        | integer | yes      | The ID of a project       |
-| `ref_name`  | string  | yes      | The ref from a repository |
-| `job`       | string  | yes      | The name of the job       |
-
-Example request:
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test"
-```
-
-Example response:
-
-| Status    | Description                     |
-|-----------|---------------------------------|
-| 200       | Serves the artifacts file       |
-| 404       | Build not found or no artifacts |
-
-[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
-
-## Get a trace file
-
-Get a trace of a specific build of a project
-
-```
-GET /projects/:id/builds/:build_id/trace
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| id         | integer | yes      | The ID of a project |
-| build_id   | integer | yes      | The ID of a build   |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
-```
-
-Response:
-
-| Status    | Description                       |
-|-----------|-----------------------------------|
-| 200       | Serves the trace file             |
-| 404       | Build not found or no trace file  |
-
-## Cancel a build
-
-Cancel a single build of a project
-
-```
-POST /projects/:id/builds/:build_id/cancel
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| `id`       | integer | yes      | The ID of a project |
-| `build_id` | integer | yes      | The ID of a build   |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
-```
-
-Example of response
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "created_at": "2016-01-11T10:13:33.506Z",
-  "artifacts_file": null,
-  "finished_at": "2016-01-11T10:14:09.526Z",
-  "id": 69,
-  "name": "rubocop",
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "started_at": null,
-  "status": "canceled",
-  "tag": false,
-  "user": null
-}
-```
-
-## Retry a build
-
-Retry a single build of a project
-
-```
-POST /projects/:id/builds/:build_id/retry
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| `id`       | integer | yes      | The ID of a project |
-| `build_id` | integer | yes      | The ID of a build   |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
-```
-
-Example of response
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "created_at": "2016-01-11T10:13:33.506Z",
-  "artifacts_file": null,
-  "finished_at": null,
-  "id": 69,
-  "name": "rubocop",
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "started_at": null,
-  "status": "pending",
-  "tag": false,
-  "user": null
-}
-```
-
-## Erase a build
-
-Erase a single build of a project (remove build artifacts and a build trace)
-
-```
-POST /projects/:id/builds/:build_id/erase
-```
-
-Parameters
-
-| Attribute   | Type    | Required | Description         |
-|-------------|---------|----------|---------------------|
-| `id`        | integer | yes      | The ID of a project |
-| `build_id`  | integer | yes      | The ID of a build   |
-
-Example of request
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
-```
-
-Example of response
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "download_url": null,
-  "id": 69,
-  "name": "rubocop",
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "created_at": "2016-01-11T10:13:33.506Z",
-  "started_at": "2016-01-11T10:13:33.506Z",
-  "finished_at": "2016-01-11T10:15:10.506Z",
-  "status": "failed",
-  "tag": false,
-  "user": null
-}
-```
-
-## Keep artifacts
-
-Prevents artifacts from being deleted when expiration is set.
-
-```
-POST /projects/:id/builds/:build_id/artifacts/keep
-```
-
-Parameters
-
-| Attribute   | Type    | Required | Description         |
-|-------------|---------|----------|---------------------|
-| `id`        | integer | yes      | The ID of a project |
-| `build_id`  | integer | yes      | The ID of a build   |
-
-Example request:
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
-```
-
-Example response:
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "download_url": null,
-  "id": 69,
-  "name": "rubocop",
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "created_at": "2016-01-11T10:13:33.506Z",
-  "started_at": "2016-01-11T10:13:33.506Z",
-  "finished_at": "2016-01-11T10:15:10.506Z",
-  "status": "failed",
-  "tag": false,
-  "user": null
-}
-```
-
-## Play a build
-
-Triggers a manual action to start a build.
-
-```
-POST /projects/:id/builds/:build_id/play
-```
-
-| Attribute  | Type    | Required | Description         |
-|------------|---------|----------|---------------------|
-| `id`       | integer | yes      | The ID of a project |
-| `build_id` | integer | yes      | The ID of a build   |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
-```
-
-Example of response
-
-```json
-{
-  "commit": {
-    "author_email": "admin@example.com",
-    "author_name": "Administrator",
-    "created_at": "2015-12-24T16:51:14.000+01:00",
-    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
-    "message": "Test the CI integration.",
-    "short_id": "0ff3ae19",
-    "title": "Test the CI integration."
-  },
-  "coverage": null,
-  "created_at": "2016-01-11T10:13:33.506Z",
-  "artifacts_file": null,
-  "finished_at": null,
-  "id": 69,
-  "name": "rubocop",
-  "ref": "master",
-  "runner": null,
-  "stage": "test",
-  "started_at": null,
-  "status": "started",
-  "tag": false,
-  "user": null
-}
-```
+This document was moved to [another location](jobs.md).
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
index b6d79706a843116d03ecf75b2041a624b742c316..c8374d94716fa16a61746a3d9b3fc5e037d7dcc9 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -5,7 +5,7 @@ API used by runners to receive and update builds.
 >**Note:**
 This API is intended to be used only by Runners as their own
 communication channel. For the consumer API see the
-[Builds API](../builds.md).
+[Jobs API](../jobs.md).
 
 ## Authentication
 
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
index 0c96b3ee335778cb1d7c11fb14d86098fde4aec4..74def20781608a9ea4446a1310d386e9eea3fed8 100644
--- a/doc/api/ci/lint.md
+++ b/doc/api/ci/lint.md
@@ -13,7 +13,7 @@ POST ci/lint
 | `content`  | string    | yes      | the .gitlab-ci.yaml content|
 
 ```bash
-curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
+curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
 ```
 
 Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 18bc2873678737dc690b06254ac8114058640dec..24c402346b14f00fb3dcf4b10455fcd1be8d80d7 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -16,7 +16,7 @@ GET /projects/:id/repository/commits
 | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
 ```
 
 Example response:
@@ -114,7 +114,7 @@ PAYLOAD=$(cat << 'JSON'
 }
 JSON
 )
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v4/projects/1/repository/commits
 ```
 
 Example response:
@@ -159,7 +159,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master
 ```
 
 Example response:
@@ -208,7 +208,7 @@ Parameters:
 | `branch` | string | yes | The name of the branch  |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/cherry_pick"
 ```
 
 Example response:
@@ -249,7 +249,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/diff"
 ```
 
 Example response:
@@ -285,7 +285,7 @@ Parameters:
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/comments"
 ```
 
 Example response:
@@ -338,7 +338,7 @@ POST /projects/:id/repository/commits/:sha/comments
 | `line_type` | string  | no  | The line type. Takes `new` or `old` as arguments |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
 ```
 
 Example response:
@@ -383,7 +383,7 @@ GET /projects/:id/repository/commits/:sha/statuses
 | `all`     | boolean | no  | Return all statuses, not only the latest ones
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
 ```
 
 Example response:
@@ -459,7 +459,7 @@ POST /projects/:id/statuses/:sha
 | `coverage` | float  | no    | The total code coverage
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
 ```
 
 Example response:
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index 73cb4b7ea8c277ef2cc5e7e95c8cdf423fff52b2..f94dbfa40591548ad6e041ee742b6a7ed0a6e30f 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -7,16 +7,16 @@ First, find the ID of the projects you're interested in, by either listing all
 projects:
 
 ```
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/projects
 ```
 
 Or finding the ID of a group and then listing all projects in that group:
 
 ```
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups
 
 # For group 1234:
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups/1234
 ```
 
 With those IDs, add the same deploy key to all:
@@ -24,6 +24,6 @@ With those IDs, add the same deploy key to all:
 ```
 for project_id in 321 456 987; do
     curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \
-    --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys
+    --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v4/projects/${project_id}/deploy_keys
 done
 ```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 39afc4b2df510400a285e8ef9cab232e9f5d87c5..f051f55ac3e898721a34ce93fc4e7b20dd9a37d4 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -9,7 +9,7 @@ GET /deploy_keys
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/deploy_keys"
 ```
 
 Example response:
@@ -46,7 +46,7 @@ GET /projects/:id/deploy_keys
 | `id` | integer | yes | The ID of the project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys"
 ```
 
 Example response:
@@ -86,7 +86,7 @@ Parameters:
 | `key_id`  | integer | yes | The ID of the deploy key |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/11"
 ```
 
 Example response:
@@ -120,7 +120,7 @@ POST /projects/:id/deploy_keys
 | `can_push` | boolean | no  | Can deploy key push to the project's repository |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v4/projects/5/deploy_keys/"
 ```
 
 Example response:
@@ -149,19 +149,7 @@ DELETE /projects/:id/deploy_keys/:key_id
 | `key_id`  | integer | yes | The ID of the deploy key |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
-```
-
-Example response:
-
-```json
-{
-  "id": 6,
-  "deploy_key_id": 14,
-  "project_id": 1,
-  "created_at" : "2015-08-29T12:50:57.259Z",
-  "updated_at" : "2015-08-29T12:50:57.259Z"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/13"
 ```
 
 ## Enable a deploy key
@@ -169,7 +157,7 @@ Example response:
 Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful.
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/deploy_keys/13/enable
 ```
 
 | Attribute | Type | Required | Description |
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 3d95c4cde604add0089bddf52f8f5bab306ca983..76e18c8a9bdd1ba0aa06ae51fbd8b67bc474e7f6 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -13,7 +13,7 @@ GET /projects/:id/deployments
 | `id`      | integer | yes      | The ID of a project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments"
 ```
 
 Example of response
@@ -151,7 +151,7 @@ GET /projects/:id/deployments/:deployment_id
 | `deployment_id` | integer | yes      | The ID of the deployment |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments/1"
 ```
 
 Example of response
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index e0ee20d96108d90fbd30b348a99a27a7b3924d25..3f0a8d989f9ab543e3fe2dca7b4b8336a88198bb 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -13,7 +13,7 @@ GET /projects/:id/environments
 | `id`      | integer | yes      | The ID of the project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/environments
 ```
 
 Example response:
@@ -33,7 +33,7 @@ Example response:
 
 Creates a new environment with the given name and external_url.
 
-It returns 201 if the environment was successfully created, 400 for wrong parameters.
+It returns `201` if the environment was successfully created, `400` for wrong parameters.
 
 ```
 POST /projects/:id/environment
@@ -46,7 +46,7 @@ POST /projects/:id/environment
 | `external_url` | string  | no     | Place to link to for this environment |
 
 ```bash
-curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments"
+curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments"
 ```
 
 Example response:
@@ -64,7 +64,7 @@ Example response:
 
 Updates an existing environment's name and/or external_url.
 
-It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned.
+It returns `200` if the environment was successfully updated. In case of an error, a status code `400` is returned.
 
 ```
 PUT /projects/:id/environments/:environments_id
@@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id
 | `external_url`  | string  | no                                | The new external_url             |
 
 ```bash
-curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
+curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1"
 ```
 
 Example response:
@@ -94,7 +94,7 @@ Example response:
 
 ## Delete an environment
 
-It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist.
+It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist.
 
 ```
 DELETE /projects/:id/environments/:environment_id
@@ -106,7 +106,24 @@ DELETE /projects/:id/environments/:environment_id
 | `environment_id` | integer | yes | The ID of the environment |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1"
+```
+
+## Stop an environment
+
+It returns `200` if the environment was successfully stopped, and `404` if the environment does not exist.
+
+```
+POST /projects/:id/environments/:environment_id/stop
+```
+
+| Attribute | Type    | Required | Description           |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
+| `environment_id` | integer | yes | The ID of the environment |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop"
 ```
 
 Example response:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 4a39dbc555532b167800893b8c107fc89f5c266c..dfc6b80bfd9858e5d0c334d7cc3d1fbbbf2e9957 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -14,6 +14,7 @@ Parameters:
 | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
 | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
 | `statistics` | boolean | no | Include group statistics (admins only) |
+| `owned` | boolean | no | Limit by groups owned by the current user |
 
 ```
 GET /groups
@@ -26,7 +27,7 @@ GET /groups
     "name": "Foobar Group",
     "path": "foo-bar",
     "description": "An interesting group",
-    "visibility_level": 20,
+    "visibility": "public",
     "lfs_enabled": true,
     "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
     "web_url": "http://localhost:3000/groups/foo-bar",
@@ -40,20 +41,6 @@ GET /groups
 
 You can search for groups by name or path, see below.
 
-## List owned groups
-
-Get a list of groups which are owned by the authenticated user.
-
-```
-GET /groups/owned
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `statistics` | boolean | no | Include group statistics |
-
 ## List a group's projects
 
 Get a list of projects in this group.
@@ -85,9 +72,8 @@ Example response:
     "description": "foo",
     "default_branch": "master",
     "tag_list": [],
-    "public": false,
     "archived": false,
-    "visibility_level": 10,
+    "visibility": "internal",
     "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
     "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
     "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
@@ -98,7 +84,7 @@ Example response:
     "issues_enabled": true,
     "merge_requests_enabled": true,
     "wiki_enabled": true,
-    "builds_enabled": true,
+    "jobs_enabled": true,
     "snippets_enabled": true,
     "created_at": "2016-04-05T21:40:50.169Z",
     "last_activity_at": "2016-04-06T16:52:08.432Z",
@@ -114,7 +100,7 @@ Example response:
     "star_count": 1,
     "forks_count": 0,
     "open_issues_count": 3,
-    "public_builds": true,
+    "public_jobs": true,
     "shared_with_groups": [],
     "request_access_enabled": false
   }
@@ -136,7 +122,7 @@ Parameters:
 | `id` | integer/string | yes | The ID or path of a group |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
 ```
 
 Example response:
@@ -147,7 +133,7 @@ Example response:
   "name": "Twitter",
   "path": "twitter",
   "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
-  "visibility_level": 20,
+  "visibility": "public",
   "avatar_url": null,
   "web_url": "https://gitlab.example.com/groups/twitter",
   "request_access_enabled": false,
@@ -160,9 +146,8 @@ Example response:
       "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
       "default_branch": "master",
       "tag_list": [],
-      "public": true,
       "archived": false,
-      "visibility_level": 20,
+      "visibility": "public",
       "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
       "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
       "web_url": "https://gitlab.example.com/twitter/typeahead-js",
@@ -173,7 +158,7 @@ Example response:
       "issues_enabled": true,
       "merge_requests_enabled": true,
       "wiki_enabled": true,
-      "builds_enabled": true,
+      "jobs_enabled": true,
       "snippets_enabled": false,
       "container_registry_enabled": true,
       "created_at": "2016-06-17T07:47:25.578Z",
@@ -190,7 +175,7 @@ Example response:
       "star_count": 0,
       "forks_count": 0,
       "open_issues_count": 3,
-      "public_builds": true,
+      "public_jobs": true,
       "shared_with_groups": [],
       "request_access_enabled": false
     },
@@ -199,9 +184,8 @@ Example response:
       "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
       "default_branch": "master",
       "tag_list": [],
-      "public": false,
       "archived": false,
-      "visibility_level": 10,
+      "visibility": "internal",
       "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
       "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
       "web_url": "https://gitlab.example.com/twitter/flight",
@@ -212,7 +196,7 @@ Example response:
       "issues_enabled": true,
       "merge_requests_enabled": true,
       "wiki_enabled": true,
-      "builds_enabled": true,
+      "jobs_enabled": true,
       "snippets_enabled": false,
       "container_registry_enabled": true,
       "created_at": "2016-06-17T07:47:24.661Z",
@@ -229,7 +213,7 @@ Example response:
       "star_count": 0,
       "forks_count": 0,
       "open_issues_count": 8,
-      "public_builds": true,
+      "public_jobs": true,
       "shared_with_groups": [],
       "request_access_enabled": false
     }
@@ -240,9 +224,8 @@ Example response:
       "description": "Velit eveniet provident fugiat saepe eligendi autem.",
       "default_branch": "master",
       "tag_list": [],
-      "public": false,
       "archived": false,
-      "visibility_level": 0,
+      "visibility": "private",
       "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
       "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
       "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
@@ -253,7 +236,7 @@ Example response:
       "issues_enabled": true,
       "merge_requests_enabled": true,
       "wiki_enabled": true,
-      "builds_enabled": true,
+      "jobs_enabled": true,
       "snippets_enabled": false,
       "container_registry_enabled": true,
       "created_at": "2016-06-17T07:47:27.089Z",
@@ -270,7 +253,7 @@ Example response:
       "star_count": 0,
       "forks_count": 0,
       "open_issues_count": 4,
-      "public_builds": true,
+      "public_jobs": true,
       "shared_with_groups": [
         {
           "group_id": 4,
@@ -301,7 +284,7 @@ Parameters:
 - `name` (required) - The name of the group
 - `path` (required) - The path of the group
 - `description` (optional) - The group's description
-- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+- `visibility` (optional) - The group's visibility. Can be `private`, `internal`, or `public`.
 - `lfs_enabled` (optional)      - Enable/disable Large File Storage (LFS) for the projects in this group
 - `request_access_enabled` (optional) - Allow users to request member access.
 - `parent_id` (optional) - The parent group id for creating nested group.
@@ -333,12 +316,12 @@ PUT /groups/:id
 | `name` | string | no | The name of the group |
 | `path` | string | no | The path of the group |
 | `description` | string | no | The description of the group |
-| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
 | `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
 | `request_access_enabled` | boolean | no | Allow users to request member access. |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
 
 ```
 
@@ -350,7 +333,7 @@ Example response:
   "name": "Experimental",
   "path": "h5bp",
   "description": "foo",
-  "visibility_level": 10,
+  "visibility": "internal",
   "avatar_url": null,
   "web_url": "http://gitlab.example.com/groups/h5bp",
   "request_access_enabled": false,
@@ -365,7 +348,7 @@ Example response:
       "tag_list": [],
       "public": false,
       "archived": false,
-      "visibility_level": 10,
+      "visibility": "internal",
       "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
       "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
       "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
@@ -376,7 +359,7 @@ Example response:
       "issues_enabled": true,
       "merge_requests_enabled": true,
       "wiki_enabled": true,
-      "builds_enabled": true,
+      "jobs_enabled": true,
       "snippets_enabled": true,
       "created_at": "2016-04-05T21:40:50.169Z",
       "last_activity_at": "2016-04-06T16:52:08.432Z",
@@ -392,7 +375,7 @@ Example response:
       "star_count": 1,
       "forks_count": 0,
       "open_issues_count": 3,
-      "public_builds": true,
+      "public_jobs": true,
       "shared_with_groups": [],
       "request_access_enabled": false
     }
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 6cd701215e90063ef40969af668992eefa6223c8..a19c965a8c3b336fdb3420a1d8f22bed71c6b3d6 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -25,18 +25,20 @@ GET /issues?labels=foo,bar
 GET /issues?labels=foo,bar&state=opened
 GET /issues?milestone=1.0.0
 GET /issues?milestone=1.0.0&state=opened
+GET /issues?iids[]=42&iids[]=43
 ```
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `state`   | string  | no    | Return all issues or just those that are `opened` or `closed`|
-| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
 | `milestone` | string| no    | The milestone title |
+| `iids`    | Array[integer] | no | Return only the issues having the given `iid` |
 | `order_by`| string  | no    | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
 | `sort`    | string  | no    | Return requests sorted in `asc` or `desc` order. Default is `desc`  |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues
 ```
 
 Example response:
@@ -80,7 +82,6 @@ Example response:
       "created_at" : "2016-01-04T15:31:51.081Z",
       "iid" : 6,
       "labels" : [],
-      "subscribed" : false,
       "user_notes_count": 1,
       "due_date": "2016-07-22",
       "web_url": "http://example.com/example/example/issues/6",
@@ -102,20 +103,22 @@ GET /groups/:id/issues?labels=foo,bar
 GET /groups/:id/issues?labels=foo,bar&state=opened
 GET /groups/:id/issues?milestone=1.0.0
 GET /groups/:id/issues?milestone=1.0.0&state=opened
+GET /groups/:id/issues?iids[]=42&iids[]=43
 ```
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `id`      | integer | yes   | The ID of a group |
 | `state`   | string  | no    | Return all issues or just those that are `opened` or `closed`|
-| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
+| `iids`    | Array[integer] | no | Return only the issues having the given `iid` |
 | `milestone` | string| no    | The milestone title |
 | `order_by`| string  | no    | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
 | `sort`    | string  | no    | Return requests sorted in `asc` or `desc` order. Default is `desc`  |
 
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4/issues
 ```
 
 Example response:
@@ -159,7 +162,6 @@ Example response:
       "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
       "updated_at" : "2016-01-04T15:31:46.176Z",
       "created_at" : "2016-01-04T15:31:46.176Z",
-      "subscribed" : false,
       "user_notes_count": 1,
       "due_date": null,
       "web_url": "http://example.com/example/example/issues/1",
@@ -181,21 +183,22 @@ GET /projects/:id/issues?labels=foo,bar
 GET /projects/:id/issues?labels=foo,bar&state=opened
 GET /projects/:id/issues?milestone=1.0.0
 GET /projects/:id/issues?milestone=1.0.0&state=opened
+GET /projects/:id/issues?iids[]=42&iids[]=43
 ```
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `id`      | integer | yes   | The ID of a project |
-| `iid`     | integer | no    | Return the issue having the given `iid` |
+| `iids`    | Array[integer] | no | Return only the milestone having the given `iid` |
 | `state`   | string  | no    | Return all issues or just those that are `opened` or `closed`|
-| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels`  | string  | no    | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
 | `milestone` | string| no    | The milestone title |
 | `order_by`| string  | no    | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
 | `sort`    | string  | no    | Return requests sorted in `asc` or `desc` order. Default is `desc`  |
 
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues
 ```
 
 Example response:
@@ -239,7 +242,6 @@ Example response:
       "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
       "updated_at" : "2016-01-04T15:31:46.176Z",
       "created_at" : "2016-01-04T15:31:46.176Z",
-      "subscribed" : false,
       "user_notes_count": 1,
       "due_date": "2016-07-22",
       "web_url": "http://example.com/example/example/issues/1",
@@ -253,16 +255,16 @@ Example response:
 Get a single project issue.
 
 ```
-GET /projects/:id/issues/:issue_id
+GET /projects/:id/issues/:issue_iid
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id`| integer | yes   | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/41
 ```
 
 Example response:
@@ -321,21 +323,22 @@ Creates a new project issue.
 POST /projects/:id/issues
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | integer | yes | The ID of a project |
-| `title`         | string  | yes | The title of an issue |
-| `description`   | string  | no  | The description of an issue  |
-| `confidential`  | boolean | no  | Set an issue to be confidential. Default is `false`.  |
-| `assignee_id`   | integer | no  | The ID of a user to assign issue |
-| `milestone_id`  | integer | no  | The ID of a milestone to assign issue |
-| `labels`        | string  | no  | Comma-separated label names for an issue  |
-| `created_at`    | string  | no  | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date`      | string  | no  | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
-| `merge_request_for_resolving_discussions` | integer | no       | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| Attribute                                 | Type    | Required | Description                                                                                                                                                                                                                                          |
+| ---------                                 | ----    | -------- | -----------                                                                                                                                                                                                                                          |
+| `id`                                      | integer | yes      | The ID of a project                                                                                                                                                                                                                                  |
+| `title`                                   | string  | yes      | The title of an issue                                                                                                                                                                                                                                |
+| `description`                             | string  | no       | The description of an issue                                                                                                                                                                                                                          |
+| `confidential`                            | boolean | no       | Set an issue to be confidential. Default is `false`.                                                                                                                                                                                                 |
+| `assignee_id`                             | integer | no       | The ID of a user to assign issue                                                                                                                                                                                                                     |
+| `milestone_id`                            | integer | no       | The ID of a milestone to assign issue                                                                                                                                                                                                                |
+| `labels`                                  | string  | no       | Comma-separated label names for an issue                                                                                                                                                                                                             |
+| `created_at`                              | string  | no       | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights)                                                                                                                                           |
+| `due_date`                                | string  | no       | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`                                                                                                                                                                                     |
+| `merge_request_to_resolve_discussions_of` | integer | no       | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| `discussion_to_resolve`                   | string  | no       | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`.                                                    |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
 ```
 
 Example response:
@@ -377,25 +380,25 @@ Updates an existing project issue. This call is also used to mark an issue as
 closed.
 
 ```
-PUT /projects/:id/issues/:issue_id
+PUT /projects/:id/issues/:issue_iid
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | integer | yes | The ID of a project |
-| `issue_id`      | integer | yes | The ID of a project's issue |
-| `title`         | string  | no  | The title of an issue |
-| `description`   | string  | no  | The description of an issue  |
-| `confidential`  | boolean | no  | Updates an issue to be confidential |
-| `assignee_id`   | integer | no  | The ID of a user to assign the issue to |
-| `milestone_id`  | integer | no  | The ID of a milestone to assign the issue to |
-| `labels`        | string  | no  | Comma-separated label names for an issue  |
-| `state_event`   | string  | no  | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
-| `updated_at`    | string  | no  | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date`      | string  | no  | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| Attribute      | Type    | Required | Description                                                                                                |
+| ---------      | ----    | -------- | -----------                                                                                                |
+| `id`           | integer | yes      | The ID of a project                                                                                        |
+| `issue_iid`    | integer | yes      | The internal ID of a project's issue                                                                       |
+| `title`        | string  | no       | The title of an issue                                                                                      |
+| `description`  | string  | no       | The description of an issue                                                                                |
+| `confidential` | boolean | no       | Updates an issue to be confidential                                                                        |
+| `assignee_id`  | integer | no       | The ID of a user to assign the issue to                                                                    |
+| `milestone_id` | integer | no       | The ID of a milestone to assign the issue to                                                               |
+| `labels`       | string  | no       | Comma-separated label names for an issue                                                                   |
+| `state_event`  | string  | no       | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it                      |
+| `updated_at`   | string  | no       | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date`     | string  | no       | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`                                           |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
 ```
 
 Example response:
@@ -436,16 +439,16 @@ Example response:
 Only for admins and project owners. Soft deletes the issue in question.
 
 ```
-DELETE /projects/:id/issues/:issue_id
+DELETE /projects/:id/issues/:issue_iid
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | integer | yes | The ID of a project |
-| `issue_id`      | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85
 ```
 
 ## Move an issue
@@ -458,17 +461,17 @@ If a given label and/or milestone with the same name also exists in the target
 project, it will then be assigned to the issue that is being moved.
 
 ```
-POST /projects/:id/issues/:issue_id/move
+POST /projects/:id/issues/:issue_iid/move
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `to_project_id` | integer | yes | The ID of the new project |
+| Attribute       | Type    | Required | Description                          |
+| ---------       | ----    | -------- | -----------                          |
+| `id`            | integer | yes      | The ID of a project                  |
+| `issue_iid`     | integer | yes      | The internal ID of a project's issue |
+| `to_project_id` | integer | yes      | The ID of the new project            |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move
 ```
 
 Example response:
@@ -514,16 +517,16 @@ If the user is already subscribed to the issue, the status code `304`
 is returned.
 
 ```
-POST /projects/:id/issues/:issue_id/subscribe
+POST /projects/:id/issues/:issue_iid/subscribe
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe
 ```
 
 Example response:
@@ -569,53 +572,16 @@ from it. If the user is not subscribed to the issue, the
 status code `304` is returned.
 
 ```
-DELETE /projects/:id/issues/:issue_id/unsubscribe
+POST /projects/:id/issues/:issue_iid/unsubscribe
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe
-```
-
-Example response:
-
-```json
-{
-  "id": 93,
-  "iid": 12,
-  "project_id": 5,
-  "title": "Incidunt et rerum ea expedita iure quibusdam.",
-  "description": "Et cumque architecto sed aut ipsam.",
-  "state": "opened",
-  "created_at": "2016-04-05T21:41:45.217Z",
-  "updated_at": "2016-04-07T13:02:37.905Z",
-  "labels": [],
-  "milestone": null,
-  "assignee": {
-    "name": "Edwardo Grady",
-    "username": "keyon",
-    "id": 21,
-    "state": "active",
-    "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/keyon"
-  },
-  "author": {
-    "name": "Vivian Hermann",
-    "username": "orville",
-    "id": 11,
-    "state": "active",
-    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/orville"
-  },
-  "subscribed": false,
-  "due_date": null,
-  "web_url": "http://example.com/example/example/issues/12",
-  "confidential": false
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
 ```
 
 ## Create a todo
@@ -625,16 +591,16 @@ there already exists a todo for the user on that issue, status code `304` is
 returned.
 
 ```
-POST /projects/:id/issues/:issue_id/todo
+POST /projects/:id/issues/:issue_iid/todo
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/todo
 ```
 
 Example response:
@@ -716,17 +682,17 @@ Example response:
 Sets an estimated time of work for this issue.
 
 ```
-POST /projects/:id/issues/:issue_id/time_estimate
+POST /projects/:id/issues/:issue_iid/time_estimate
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute   | Type    | Required | Description                              |
+| ---------   | ----    | -------- | -----------                              |
+| `id`        | integer | yes      | The ID of a project                      |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue     |
+| `duration`  | string  | yes      | The duration in human format. e.g: 3h30m |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_estimate?duration=3h30m
 ```
 
 Example response:
@@ -745,16 +711,16 @@ Example response:
 Resets the estimated time for this issue to 0 seconds.
 
 ```
-POST /projects/:id/issues/:issue_id/reset_time_estimate
+POST /projects/:id/issues/:issue_iid/reset_time_estimate
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_time_estimate
 ```
 
 Example response:
@@ -773,17 +739,17 @@ Example response:
 Adds spent time for this issue
 
 ```
-POST /projects/:id/issues/:issue_id/add_spent_time
+POST /projects/:id/issues/:issue_iid/add_spent_time
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute   | Type    | Required | Description                              |
+| ---------   | ----    | -------- | -----------                              |
+| `id`        | integer | yes      | The ID of a project                      |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue     |
+| `duration`  | string  | yes      | The duration in human format. e.g: 3h30m |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/add_spent_time?duration=1h
 ```
 
 Example response:
@@ -802,16 +768,16 @@ Example response:
 Resets the total spent time for this issue to 0 seconds.
 
 ```
-POST /projects/:id/issues/:issue_id/reset_spent_time
+POST /projects/:id/issues/:issue_iid/reset_spent_time
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_spent_time
 ```
 
 Example response:
@@ -828,16 +794,16 @@ Example response:
 ## Get time tracking stats
 
 ```
-GET /projects/:id/issues/:issue_id/time_stats
+GET /projects/:id/issues/:issue_iid/time_stats
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute   | Type    | Required | Description                          |
+| ---------   | ----    | -------- | -----------                          |
+| `id`        | integer | yes      | The ID of a project                  |
+| `issue_iid` | integer | yes      | The internal ID of a project's issue |
 
 ```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_stats
 ```
 
 Example response:
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
new file mode 100644
index 0000000000000000000000000000000000000000..7340123e09d96bd1be64e78ad7e0b6a13b8a41b2
--- /dev/null
+++ b/doc/api/jobs.md
@@ -0,0 +1,622 @@
+# Jobs API
+
+## List project jobs
+
+Get a list of jobs in a project.
+
+```
+GET /projects/:id/jobs
+```
+
+| Attribute | Type    | Required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+| `scope`   | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
+```
+
+Example of response
+
+```json
+[
+  {
+    "commit": {
+      "author_email": "admin@example.com",
+      "author_name": "Administrator",
+      "created_at": "2015-12-24T16:51:14.000+01:00",
+      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "message": "Test the CI integration.",
+      "short_id": "0ff3ae19",
+      "title": "Test the CI integration."
+    },
+    "coverage": null,
+    "created_at": "2015-12-24T15:51:21.802Z",
+    "artifacts_file": {
+      "filename": "artifacts.zip",
+      "size": 1000
+    },
+    "finished_at": "2015-12-24T17:54:27.895Z",
+    "id": 7,
+    "name": "teaspoon",
+    "pipeline": {
+      "id": 6,
+      "ref": "master",
+      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "status": "pending"
+    },
+    "ref": "master",
+    "runner": null,
+    "stage": "test",
+    "started_at": "2015-12-24T17:54:27.722Z",
+    "status": "failed",
+    "tag": false,
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "bio": null,
+      "created_at": "2015-12-21T13:14:24.077Z",
+      "id": 1,
+      "is_admin": true,
+      "linkedin": "",
+      "name": "Administrator",
+      "skype": "",
+      "state": "active",
+      "twitter": "",
+      "username": "root",
+      "web_url": "http://gitlab.dev/root",
+      "website_url": ""
+    }
+  },
+  {
+    "commit": {
+      "author_email": "admin@example.com",
+      "author_name": "Administrator",
+      "created_at": "2015-12-24T16:51:14.000+01:00",
+      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "message": "Test the CI integration.",
+      "short_id": "0ff3ae19",
+      "title": "Test the CI integration."
+    },
+    "coverage": null,
+    "created_at": "2015-12-24T15:51:21.727Z",
+    "artifacts_file": null,
+    "finished_at": "2015-12-24T17:54:24.921Z",
+    "id": 6,
+    "name": "spinach:other",
+    "pipeline": {
+      "id": 6,
+      "ref": "master",
+      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "status": "pending"
+    },
+    "ref": "master",
+    "runner": null,
+    "stage": "test",
+    "started_at": "2015-12-24T17:54:24.729Z",
+    "status": "failed",
+    "tag": false,
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "bio": null,
+      "created_at": "2015-12-21T13:14:24.077Z",
+      "id": 1,
+      "is_admin": true,
+      "linkedin": "",
+      "name": "Administrator",
+      "skype": "",
+      "state": "active",
+      "twitter": "",
+      "username": "root",
+      "web_url": "http://gitlab.dev/root",
+      "website_url": ""
+    }
+  }
+]
+```
+
+## List pipeline jobs
+
+Get a list of jobs for a pipeline.
+
+```
+GET /projects/:id/pipeline/:pipeline_id/jobs
+```
+
+| Attribute     | Type                           | Required | Description          |
+|---------------|--------------------------------|----------|----------------------|
+| `id`          | integer                        | yes      | The ID of a project  |
+| `pipeline_id` | integer                        | yes      | The ID of a pipeline |
+| `scope`       | string **or** array of strings | no       | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running'
+```
+
+Example of response
+
+```json
+[
+  {
+    "commit": {
+      "author_email": "admin@example.com",
+      "author_name": "Administrator",
+      "created_at": "2015-12-24T16:51:14.000+01:00",
+      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "message": "Test the CI integration.",
+      "short_id": "0ff3ae19",
+      "title": "Test the CI integration."
+    },
+    "coverage": null,
+    "created_at": "2015-12-24T15:51:21.802Z",
+    "artifacts_file": {
+      "filename": "artifacts.zip",
+      "size": 1000
+    },
+    "finished_at": "2015-12-24T17:54:27.895Z",
+    "id": 7,
+    "name": "teaspoon",
+    "pipeline": {
+      "id": 6,
+      "ref": "master",
+      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "status": "pending"
+    },
+    "ref": "master",
+    "runner": null,
+    "stage": "test",
+    "started_at": "2015-12-24T17:54:27.722Z",
+    "status": "failed",
+    "tag": false,
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "bio": null,
+      "created_at": "2015-12-21T13:14:24.077Z",
+      "id": 1,
+      "is_admin": true,
+      "linkedin": "",
+      "name": "Administrator",
+      "skype": "",
+      "state": "active",
+      "twitter": "",
+      "username": "root",
+      "web_url": "http://gitlab.dev/root",
+      "website_url": ""
+    }
+  },
+  {
+    "commit": {
+      "author_email": "admin@example.com",
+      "author_name": "Administrator",
+      "created_at": "2015-12-24T16:51:14.000+01:00",
+      "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "message": "Test the CI integration.",
+      "short_id": "0ff3ae19",
+      "title": "Test the CI integration."
+    },
+    "coverage": null,
+    "created_at": "2015-12-24T15:51:21.727Z",
+    "artifacts_file": null,
+    "finished_at": "2015-12-24T17:54:24.921Z",
+    "id": 6,
+    "name": "spinach:other",
+    "pipeline": {
+      "id": 6,
+      "ref": "master",
+      "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+      "status": "pending"
+    },
+    "ref": "master",
+    "runner": null,
+    "stage": "test",
+    "started_at": "2015-12-24T17:54:24.729Z",
+    "status": "failed",
+    "tag": false,
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "bio": null,
+      "created_at": "2015-12-21T13:14:24.077Z",
+      "id": 1,
+      "is_admin": true,
+      "linkedin": "",
+      "name": "Administrator",
+      "skype": "",
+      "state": "active",
+      "twitter": "",
+      "username": "root",
+      "web_url": "http://gitlab.dev/root",
+      "website_url": ""
+    }
+  }
+]
+```
+
+## Get a single job
+
+Get a single job of a project
+
+```
+GET /projects/:id/jobs/:job_id
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `job_id` | integer | yes      | The ID of a job   |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8"
+```
+
+Example of response
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "created_at": "2015-12-24T15:51:21.880Z",
+  "artifacts_file": null,
+  "finished_at": "2015-12-24T17:54:31.198Z",
+  "id": 8,
+  "name": "rubocop",
+  "pipeline": {
+    "id": 6,
+    "ref": "master",
+    "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "status": "pending"
+  },
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "started_at": "2015-12-24T17:54:30.733Z",
+  "status": "failed",
+  "tag": false,
+  "user": {
+    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+    "bio": null,
+    "created_at": "2015-12-21T13:14:24.077Z",
+    "id": 1,
+    "is_admin": true,
+    "linkedin": "",
+    "name": "Administrator",
+    "skype": "",
+    "state": "active",
+    "twitter": "",
+    "username": "root",
+    "web_url": "http://gitlab.dev/root",
+    "website_url": ""
+  }
+}
+```
+
+## Get job artifacts
+
+> [Introduced][ce-2893] in GitLab 8.5
+
+Get job artifacts of a project
+
+```
+GET /projects/:id/jobs/:job_id/artifacts
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `job_id` | integer | yes      | The ID of a job   |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
+```
+
+Response:
+
+| Status    | Description                     |
+|-----------|---------------------------------|
+| 200       | Serves the artifacts file       |
+| 404       | Build not found or no artifacts |
+
+[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
+
+## Download the artifacts file
+
+> [Introduced][ce-5347] in GitLab 8.10.
+
+Download the artifacts file from the given reference name and job provided the
+job finished successfully.
+
+```
+GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
+```
+
+Parameters
+
+| Attribute   | Type    | Required | Description               |
+|-------------|---------|----------|-------------------------- |
+| `id`        | integer | yes      | The ID of a project       |
+| `ref_name`  | string  | yes      | The ref from a repository |
+| `job`       | string  | yes      | The name of the job       |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
+```
+
+Example response:
+
+| Status    | Description                     |
+|-----------|---------------------------------|
+| 200       | Serves the artifacts file       |
+| 404       | Build not found or no artifacts |
+
+[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
+
+## Get a trace file
+
+Get a trace of a specific job of a project
+
+```
+GET /projects/:id/jobs/:job_id/trace
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| id         | integer | yes      | The ID of a project |
+| job_id     | integer | yes      | The ID of a job     |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace"
+```
+
+Response:
+
+| Status    | Description                       |
+|-----------|-----------------------------------|
+| 200       | Serves the trace file             |
+| 404       | Build not found or no trace file  |
+
+## Cancel a job
+
+Cancel a single job of a project
+
+```
+POST /projects/:id/jobs/:job_id/cancel
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `job_id`   | integer | yes      | The ID of a job     |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/cancel"
+```
+
+Example of response
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "created_at": "2016-01-11T10:13:33.506Z",
+  "artifacts_file": null,
+  "finished_at": "2016-01-11T10:14:09.526Z",
+  "id": 69,
+  "name": "rubocop",
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "started_at": null,
+  "status": "canceled",
+  "tag": false,
+  "user": null
+}
+```
+
+## Retry a job
+
+Retry a single job of a project
+
+```
+POST /projects/:id/jobs/:job_id/retry
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `job_id`   | integer | yes      | The ID of a job     |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/retry"
+```
+
+Example of response
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "created_at": "2016-01-11T10:13:33.506Z",
+  "artifacts_file": null,
+  "finished_at": null,
+  "id": 69,
+  "name": "rubocop",
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "started_at": null,
+  "status": "pending",
+  "tag": false,
+  "user": null
+}
+```
+
+## Erase a job
+
+Erase a single job of a project (remove job artifacts and a job trace)
+
+```
+POST /projects/:id/jobs/:job_id/erase
+```
+
+Parameters
+
+| Attribute   | Type    | Required | Description         |
+|-------------|---------|----------|---------------------|
+| `id`        | integer | yes      | The ID of a project |
+| `job_id`    | integer | yes      | The ID of a job     |
+
+Example of request
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/erase"
+```
+
+Example of response
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "download_url": null,
+  "id": 69,
+  "name": "rubocop",
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "created_at": "2016-01-11T10:13:33.506Z",
+  "started_at": "2016-01-11T10:13:33.506Z",
+  "finished_at": "2016-01-11T10:15:10.506Z",
+  "status": "failed",
+  "tag": false,
+  "user": null
+}
+```
+
+## Keep artifacts
+
+Prevents artifacts from being deleted when expiration is set.
+
+```
+POST /projects/:id/jobs/:job_id/artifacts/keep
+```
+
+Parameters
+
+| Attribute   | Type    | Required | Description         |
+|-------------|---------|----------|---------------------|
+| `id`        | integer | yes      | The ID of a project |
+| `job_id`    | integer | yes      | The ID of a job     |
+
+Example request:
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep"
+```
+
+Example response:
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "download_url": null,
+  "id": 69,
+  "name": "rubocop",
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "created_at": "2016-01-11T10:13:33.506Z",
+  "started_at": "2016-01-11T10:13:33.506Z",
+  "finished_at": "2016-01-11T10:15:10.506Z",
+  "status": "failed",
+  "tag": false,
+  "user": null
+}
+```
+
+## Play a job
+
+Triggers a manual action to start a job.
+
+```
+POST /projects/:id/jobs/:job_id/play
+```
+
+| Attribute | Type    | Required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+| `job_id`  | integer | yes      | The ID of a job     |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/play"
+```
+
+Example of response
+
+```json
+{
+  "commit": {
+    "author_email": "admin@example.com",
+    "author_name": "Administrator",
+    "created_at": "2015-12-24T16:51:14.000+01:00",
+    "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+    "message": "Test the CI integration.",
+    "short_id": "0ff3ae19",
+    "title": "Test the CI integration."
+  },
+  "coverage": null,
+  "created_at": "2016-01-11T10:13:33.506Z",
+  "artifacts_file": null,
+  "finished_at": null,
+  "id": 69,
+  "name": "rubocop",
+  "ref": "master",
+  "runner": null,
+  "stage": "test",
+  "started_at": null,
+  "status": "started",
+  "tag": false,
+  "user": null
+}
+```
diff --git a/doc/api/labels.md b/doc/api/labels.md
index a1e7eb1a7b18c0520103620e8e857000b3f1329f..e8c220f6809f025ed77b42647deafb7de730ffa4 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -13,7 +13,7 @@ GET /projects/:id/labels
 | `id`      | integer | yes      | The ID of the project |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/labels
 ```
 
 Example response:
@@ -95,7 +95,7 @@ POST /projects/:id/labels
 | `priority`    | integer | no       | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
 
 ```bash
-curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels"
 ```
 
 Example response:
@@ -128,23 +128,7 @@ DELETE /projects/:id/labels
 | `name`    | string  | yes      | The name of the label |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
-```
-
-Example response:
-
-```json
-{
-  "id" : 1,
-  "name" : "bug",
-  "color" : "#d9534f",
-  "description": "Bug reported by user",
-  "open_issues_count": 1,
-  "closed_issues_count": 0,
-  "open_merge_requests_count": 1,
-  "subscribed": false,
-  "priority": null
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels?name=bug"
 ```
 
 ## Edit an existing label
@@ -167,7 +151,7 @@ PUT /projects/:id/labels
 
 
 ```bash
-curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels"
 ```
 
 Example response:
@@ -202,7 +186,7 @@ POST /projects/:id/labels/:label_id/subscribe
 | `label_id` | integer or string | yes      | The ID or title of a project's label |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/subscribe
 ```
 
 Example response:
@@ -228,7 +212,7 @@ from it. If the user is not subscribed to the label, the
 status code `304` is returned.
 
 ```
-DELETE /projects/:id/labels/:label_id/unsubscribe
+POST /projects/:id/labels/:label_id/unsubscribe
 ```
 
 | Attribute  | Type              | Required | Description                          |
@@ -237,21 +221,5 @@ DELETE /projects/:id/labels/:label_id/unsubscribe
 | `label_id` | integer or string | yes      | The ID or title of a project's label |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe
-```
-
-Example response:
-
-```json
-{
-  "id" : 1,
-  "name" : "bug",
-  "color" : "#d9534f",
-  "description": "Bug reported by user",
-  "open_issues_count": 1,
-  "closed_issues_count": 0,
-  "open_merge_requests_count": 1,
-  "subscribed": false,
-  "priority": null
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/unsubscribe
 ```
diff --git a/doc/api/members.md b/doc/api/members.md
index 5dcb2a5f60aa00236efd8633e639f62be60cc65a..fe46f8f84bcf7a92156793522008a8022b5c781b 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -27,8 +27,8 @@ GET /projects/:id/members
 | `query`   | string | no     | A query string to search for members |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members
 ```
 
 Example response:
@@ -69,8 +69,8 @@ GET /projects/:id/members/:user_id
 | `user_id` | integer | yes   | The user ID of the member |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id
 ```
 
 Example response:
@@ -104,8 +104,8 @@ POST /projects/:id/members
 | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/groups/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/projects/:id/members
 ```
 
 Example response:
@@ -138,8 +138,8 @@ PUT /projects/:id/members/:user_id
 | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id?access_level=40
 ```
 
 Example response:
@@ -170,6 +170,6 @@ DELETE /projects/:id/members/:user_id
 | `user_id` | integer | yes   | The user ID of the member |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id
 ```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2a99ae822d7c86afe28591625f52a01a996991c5..2e0545da1c42099f4a613499a11c6d72271dbe9d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -65,9 +65,8 @@ Parameters:
       "updated_at": "2015-02-02T19:49:26.013Z",
       "due_date": null
     },
-    "merge_when_build_succeeds": true,
+    "merge_when_pipeline_succeeds": true,
     "merge_status": "can_be_merged",
-    "subscribed" : false,
     "sha": "8888888888888888888888888888888888888888",
     "merge_commit_sha": null,
     "user_notes_count": 1,
@@ -83,13 +82,13 @@ Parameters:
 Shows information about a single merge request.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id
+GET /projects/:id/merge_requests/:merge_request_iid
 ```
 
 Parameters:
 
 - `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
 
 ```json
 {
@@ -134,7 +133,7 @@ Parameters:
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -151,13 +150,13 @@ Parameters:
 Get a list of merge request commits.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/commits
+GET /projects/:id/merge_requests/:merge_request_iid/commits
 ```
 
 Parameters:
 
 - `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
 
 
 ```json
@@ -188,13 +187,13 @@ Parameters:
 Shows information about the merge request including its files and changes.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/changes
+GET /projects/:id/merge_requests/:merge_request_iid/changes
 ```
 
 Parameters:
 
 - `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
 
 ```json
 {
@@ -239,7 +238,7 @@ Parameters:
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -270,18 +269,18 @@ Creates a new merge request.
 POST /projects/:id/merge_requests
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | string  | yes | The ID of a project |
-| `source_branch` | string  | yes | The source branch |
-| `target_branch` | string  | yes | The target branch |
-| `title`         | string  | yes | Title of MR |
-| `assignee_id`   | integer | no  | Assignee user ID |
-| `description`   | string  | no  | Description of MR |
-| `target_project_id` | integer  | no | The target project (numeric id) |
-| `labels` | string  | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer  | no | The ID of a milestone |
-| `remove_source_branch` | boolean  | no | Flag indicating if a merge request should remove the source branch when merging |
+| Attribute              | Type    | Required | Description                                                                     |
+| ---------              | ----    | -------- | -----------                                                                     |
+| `id`                   | string  | yes      | The ID of a project                                                             |
+| `source_branch`        | string  | yes      | The source branch                                                               |
+| `target_branch`        | string  | yes      | The target branch                                                               |
+| `title`                | string  | yes      | Title of MR                                                                     |
+| `assignee_id`          | integer | no       | Assignee user ID                                                                |
+| `description`          | string  | no       | Description of MR                                                               |
+| `target_project_id`    | integer | no       | The target project (numeric id)                                                 |
+| `labels`               | string  | no       | Labels for MR as a comma-separated list                                         |
+| `milestone_id`         | integer | no       | The ID of a milestone                                                           |
+| `remove_source_branch` | boolean | no       | Flag indicating if a merge request should remove the source branch when merging |
 
 ```json
 {
@@ -326,7 +325,7 @@ POST /projects/:id/merge_requests
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -343,21 +342,21 @@ POST /projects/:id/merge_requests
 Updates an existing merge request. You can change the target branch, title, or even close the MR.
 
 ```
-PUT /projects/:id/merge_requests/:merge_request_id
+PUT /projects/:id/merge_requests/:merge_request_iid
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | string  | yes | The ID of a project |
-| `merge_request_id` | integer  | yes | The ID of a merge request |
-| `target_branch` | string  | no | The target branch |
-| `title`         | string  | no | Title of MR |
-| `assignee_id`   | integer | no  | Assignee user ID |
-| `description`   | string  | no  | Description of MR |
-| `state_event` | string  | no | New state (close/reopen) |
-| `labels` | string  | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer  | no | The ID of a milestone |
-| `remove_source_branch` | boolean  | no | Flag indicating if a merge request should remove the source branch when merging |
+| Attribute              | Type    | Required | Description                                                                     |
+| ---------              | ----    | -------- | -----------                                                                     |
+| `id`                   | string  | yes      | The ID of a project                                                             |
+| `merge_request_iid`    | integer | yes      | The ID of a merge request                                                       |
+| `target_branch`        | string  | no       | The target branch                                                               |
+| `title`                | string  | no       | Title of MR                                                                     |
+| `assignee_id`          | integer | no       | Assignee user ID                                                                |
+| `description`          | string  | no       | Description of MR                                                               |
+| `state_event`          | string  | no       | New state (close/reopen)                                                        |
+| `labels`               | string  | no       | Labels for MR as a comma-separated list                                         |
+| `milestone_id`         | integer | no       | The ID of a milestone                                                           |
+| `remove_source_branch` | boolean | no       | Flag indicating if a merge request should remove the source branch when merging |
 
 Must include at least one non-required attribute from above.
 
@@ -403,7 +402,7 @@ Must include at least one non-required attribute from above.
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -420,16 +419,16 @@ Must include at least one non-required attribute from above.
 Only for admins and project owners. Soft deletes the merge request in question.
 
 ```
-DELETE /projects/:id/merge_requests/:merge_request_id
+DELETE /projects/:id/merge_requests/:merge_request_iid
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`            | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| Attribute           | Type    | Required | Description                          |
+| ---------           | ----    | -------- | -----------                          |
+| `id`                | integer | yes      | The ID of a project                  |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/merge_requests/85
 ```
 
 ## Accept MR
@@ -446,16 +445,16 @@ If the `sha` parameter is passed and does not match the HEAD of the source - you
 If you don't have permissions to accept this merge request - you'll get a `401`
 
 ```
-PUT /projects/:id/merge_requests/:merge_request_id/merge
+PUT /projects/:id/merge_requests/:merge_request_iid/merge
 ```
 
 Parameters:
 
 - `id` (required)                           - The ID of a project
-- `merge_request_id` (required)             - ID of MR
+- `merge_request_iid` (required)            - Internal ID of MR
 - `merge_commit_message` (optional)         - Custom merge commit message
 - `should_remove_source_branch` (optional)  - if `true` removes the source branch
-- `merge_when_build_succeeds` (optional)    - if `true` the MR is merged when the build succeeds
+- `merge_when_pipeline_succeeds` (optional)    - if `true` the MR is merged when the pipeline succeeds
 - `sha` (optional)                          - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail
 
 ```json
@@ -501,7 +500,7 @@ Parameters:
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -519,14 +518,14 @@ If you don't have permissions to accept this merge request - you'll get a `401`
 
 If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed'
 
-In case the merge request is not set to be merged when the build succeeds, you'll also get a `406` error.
+In case the merge request is not set to be merged when the pipeline succeeds, you'll also get a `406` error.
 ```
-PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds
+PUT /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds
 ```
 Parameters:
 
 - `id` (required)                           - The ID of a project
-- `merge_request_id` (required)             - ID of MR
+- `merge_request_iid` (required)            - Internal ID of MR
 
 ```json
 {
@@ -571,7 +570,7 @@ Parameters:
     "updated_at": "2015-02-02T19:49:26.013Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": true,
+  "merge_when_pipeline_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
   "sha": "8888888888888888888888888888888888888888",
@@ -592,16 +591,16 @@ Comments are done via the [notes](notes.md) resource.
 Get all the issues that would be closed by merging the provided merge request.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes   | The ID of the merge request |
+| Attribute           | Type    | Required | Description                          |
+| ---------           | ----    | -------- | -----------                          |
+| `id`                | integer | yes      | The ID of a project                  |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues
 ```
 
 Example response when the GitLab issue tracker is used:
@@ -667,16 +666,16 @@ Subscribes the authenticated user to a merge request to receive notification. If
 status code `304` is returned.
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/subscribe
+POST /projects/:id/merge_requests/:merge_request_iid/subscribe
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes   | The ID of the merge request |
+| Attribute           | Type    | Required | Description                 |
+| ---------           | ----    | -------- | -----------                 |
+| `id`                | integer | yes      | The ID of a project         |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe
 ```
 
 Example response:
@@ -726,7 +725,7 @@ Example response:
     "updated_at": "2016-04-05T21:41:40.905Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": false,
+  "merge_when_pipeline_succeeds": false,
   "merge_status": "cannot_be_merged",
   "subscribed": true,
   "sha": "8888888888888888888888888888888888888888",
@@ -741,16 +740,16 @@ notifications from that merge request. If the user is
 not subscribed to the merge request, the status code `304` is returned.
 
 ```
-DELETE /projects/:id/merge_requests/:merge_request_id/unsubscribe
+POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes   | The ID of the merge request |
+| Attribute           | Type    | Required | Description                          |
+| ---------           | ----    | -------- | -----------                          |
+| `id`                | integer | yes      | The ID of a project                  |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/unsubscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe
 ```
 
 Example response:
@@ -800,7 +799,7 @@ Example response:
     "updated_at": "2016-04-05T21:41:40.905Z",
     "due_date": null
   },
-  "merge_when_build_succeeds": false,
+  "merge_when_pipeline_succeeds": false,
   "merge_status": "cannot_be_merged",
   "subscribed": false,
   "sha": "8888888888888888888888888888888888888888",
@@ -815,16 +814,16 @@ If there already exists a todo for the user on that merge request,
 status code `304` is returned.
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/todo
+POST /projects/:id/merge_requests/:merge_request_iid/todo
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes   | The ID of the merge request |
+| Attribute           | Type    | Required | Description                          |
+| ---------           | ----    | -------- | -----------                          |
+| `id`                | integer | yes      | The ID of a project                  |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo
 ```
 
 Example response:
@@ -893,7 +892,7 @@ Example response:
       "updated_at": "2016-06-17T07:47:33.840Z",
       "due_date": null
     },
-    "merge_when_build_succeeds": false,
+    "merge_when_pipeline_succeeds": false,
     "merge_status": "unchecked",
     "subscribed": true,
     "sha": "8888888888888888888888888888888888888888",
@@ -915,16 +914,16 @@ Example response:
 Get a list of merge request diff versions.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/versions
+GET /projects/:id/merge_requests/:merge_request_iid/versions
 ```
 
-| Attribute | Type    | Required | Description           |
-| --------- | ------- | -------- | --------------------- |
-| `id`      | String  | yes      | The ID of the project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute           | Type    | Required | Description                 |
+| ---------           | ------- | -------- | ---------------------       |
+| `id`                | String  | yes      | The ID of the project       |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions
 ```
 
 Example response:
@@ -956,17 +955,17 @@ Example response:
 Get a single merge request diff version.
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id
+GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id
 ```
 
-| Attribute | Type    | Required | Description           |
-| --------- | ------- | -------- | --------------------- |
-| `id`      | String  | yes      | The ID of the project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
-| `version_id` | integer | yes | The ID of the merge request diff version |
+| Attribute           | Type    | Required | Description                              |
+| ---------           | ------- | -------- | ---------------------                    |
+| `id`                | String  | yes      | The ID of the project                    |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request     |
+| `version_id`        | integer | yes      | The ID of the merge request diff version |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions/1
 ```
 
 Example response:
@@ -1023,17 +1022,17 @@ Example response:
 Sets an estimated time of work for this merge request.
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/time_estimate
+POST /projects/:id/merge_requests/:merge_request_iid/time_estimate
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute           | Type    | Required | Description                              |
+| ---------           | ----    | -------- | -----------                              |
+| `id`                | integer | yes      | The ID of a project                      |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request     |
+| `duration`          | string  | yes      | The duration in human format. e.g: 3h30m |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m
 ```
 
 Example response:
@@ -1052,16 +1051,16 @@ Example response:
 Resets the estimated time for this merge request to 0 seconds.
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
+POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+| Attribute           | Type    | Required | Description                                  |
+| ---------           | ----    | -------- | -----------                                  |
+| `id`                | integer | yes      | The ID of a project                          |
+| `merge_request_iid` | integer | yes      | The internal ID of a project's merge_request |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate
 ```
 
 Example response:
@@ -1080,17 +1079,17 @@ Example response:
 Adds spent time for this merge request
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
+POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute           | Type    | Required | Description                              |
+| ---------           | ----    | -------- | -----------                              |
+| `id`                | integer | yes      | The ID of a project                      |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request     |
+| `duration`          | string  | yes      | The duration in human format. e.g: 3h30m |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h
 ```
 
 Example response:
@@ -1109,16 +1108,16 @@ Example response:
 Resets the total spent time for this merge request to 0 seconds.
 
 ```
-POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
+POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+| Attribute           | Type    | Required | Description                                  |
+| ---------           | ----    | -------- | -----------                                  |
+| `id`                | integer | yes      | The ID of a project                          |
+| `merge_request_iid` | integer | yes      | The internal ID of a project's merge_request |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time
 ```
 
 Example response:
@@ -1135,16 +1134,16 @@ Example response:
 ## Get time tracking stats
 
 ```
-GET /projects/:id/merge_requests/:merge_request_id/time_stats
+GET /projects/:id/merge_requests/:merge_request_iid/time_stats
 ```
 
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| Attribute           | Type    | Required | Description                          |
+| ---------           | ----    | -------- | -----------                          |
+| `id`                | integer | yes      | The ID of a project                  |
+| `merge_request_iid` | integer | yes      | The internal ID of the merge request |
 
 ```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats
 ```
 
 Example response:
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index bf7dcc008e96c2b611640a464e4329ab9ddc6974..3c86357a6c39ae716a86392738d37e917ff89c16 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -6,10 +6,11 @@ Returns a list of project milestones.
 
 ```
 GET /projects/:id/milestones
-GET /projects/:id/milestones?iid=42
-GET /projects/:id/milestones?iid[]=42&iid[]=43
+GET /projects/:id/milestones?iids=42
+GET /projects/:id/milestones?iids[]=42&iids[]=43
 GET /projects/:id/milestones?state=active
 GET /projects/:id/milestones?state=closed
+GET /projects/:id/milestones?search=version
 ```
 
 Parameters:
@@ -17,11 +18,12 @@ Parameters:
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `id` | integer | yes | The ID of a project |
-| `iid` | Array[integer] | optional | Return only the milestone having the given `iid` |
-| `state` | string | optional | Return  only `active` or `closed` milestones` |
+| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/milestones
 ```
 
 Example Response:
@@ -115,4 +117,4 @@ GET /projects/:id/milestones/:milestone_id/merge_requests
 Parameters:
 
 - `id` (required) - The ID of a project
-- `milestone_id` (required) - The ID of a project milestone
\ No newline at end of file
+- `milestone_id` (required) - The ID of a project milestone
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 1d97b5de688401b0e789a62efc4b778e191556eb..eef06d5f324f6af2ba9b9303090729b101c6bbfd 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -19,7 +19,7 @@ GET /namespaces
 Example request:
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces
 ```
 
 Example response:
@@ -60,7 +60,7 @@ GET /namespaces?search=foobar
 Example request:
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces?search=twitter
 ```
 
 Example response:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 214dfa4068da957cbc9a0d75b2f5e91f008e316a..6ef06b2c2e95e7a72a11123d95b25b1a52f0b346 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -34,8 +34,6 @@ Parameters:
     "created_at": "2013-10-02T09:22:45Z",
     "updated_at": "2013-10-02T10:22:45Z",
     "system": true,
-    "upvote": false,
-    "downvote": false,
     "noteable_id": 377,
     "noteable_type": "Issue"
   },
@@ -54,8 +52,6 @@ Parameters:
     "created_at": "2013-10-02T09:56:03Z",
     "updated_at": "2013-10-02T09:56:03Z",
     "system": true,
-    "upvote": false,
-    "downvote": false,
     "noteable_id": 121,
     "noteable_type": "Issue"
   }
@@ -124,33 +120,7 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
-```
-
-Example Response:
-
-```json
-{
-  "id": 636,
-  "body": "This is a good idea.",
-  "attachment": null,
-  "author": {
-    "id": 1,
-    "username": "pipin",
-    "email": "admin@example.com",
-    "name": "Pip",
-    "state": "active",
-    "created_at": "2013-09-30T13:46:01Z",
-    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/pipin"
-  },
-  "created_at": "2016-04-05T22:10:44.164Z",
-  "system": false,
-  "noteable_id": 11,
-  "noteable_type": "Issue",
-  "upvote": false,
-  "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/notes/636
 ```
 
 ## Snippets
@@ -248,33 +218,7 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
-```
-
-Example Response:
-
-```json
-{
-  "id": 1659,
-  "body": "This is a good idea.",
-  "attachment": null,
-  "author": {
-    "id": 1,
-    "username": "pipin",
-    "email": "admin@example.com",
-    "name": "Pip",
-    "state": "active",
-    "created_at": "2013-09-30T13:46:01Z",
-    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/pipin"
-  },
-  "created_at": "2016-04-06T16:51:53.239Z",
-  "system": false,
-  "noteable_id": 52,
-  "noteable_type": "Snippet",
-  "upvote": false,
-  "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/52/notes/1659
 ```
 
 ## Merge Requests
@@ -322,8 +266,6 @@ Parameters:
   "created_at": "2013-10-02T08:57:14Z",
   "updated_at": "2013-10-02T08:57:14Z",
   "system": false,
-  "upvote": false,
-  "downvote": false,
   "noteable_id": 2,
   "noteable_type": "MergeRequest"
 }
@@ -377,31 +319,5 @@ Parameters:
 | `note_id` | integer | yes | The ID of a note |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
-```
-
-Example Response:
-
-```json
-{
-  "id": 1602,
-  "body": "This is a good idea.",
-  "attachment": null,
-  "author": {
-    "id": 1,
-    "username": "pipin",
-    "email": "admin@example.com",
-    "name": "Pip",
-    "state": "active",
-    "created_at": "2013-09-30T13:46:01Z",
-    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/pipin"
-  },
-  "created_at": "2016-04-05T22:11:59.923Z",
-  "system": false,
-  "noteable_id": 7,
-  "noteable_type": "MergeRequest",
-  "upvote": false,
-  "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602
 ```
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index aea1c12a3927678fe6313376753ad870921499c8..43047917f777d59278ebd4f80fad56fcc9627bfb 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -41,7 +41,7 @@ GET /notification_settings
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings
 ```
 
 Example response:
@@ -62,7 +62,7 @@ PUT /notification_settings
 ```
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings?level=watch
 ```
 
 | Attribute | Type | Required | Description |
@@ -101,8 +101,8 @@ GET /projects/:id/notification_settings
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings
 ```
 
 | Attribute | Type | Required | Description |
@@ -127,8 +127,8 @@ PUT /projects/:id/notification_settings
 ```
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings?level=watch
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings?level=custom&new_note=true
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings?level=watch
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings?level=custom&new_note=true
 ```
 
 | Attribute | Type | Required | Description |
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index eab532af594a749ae5ea73ffe9bb5503c2f19315..46fe64d382e6ef4ad775a9daecee665f2dcf90c3 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -77,13 +77,13 @@ You can now make requests to the API with the access token returned.
 The access token allows you to make requests to the API on a behalf of a user.
 
 ```
-GET https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
 ```
 
 Or you can put the token to the Authorization header:
 
 ```
-curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/user
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
 ```
 
 ## Resource Owner Password Credentials
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
new file mode 100644
index 0000000000000000000000000000000000000000..fdb41a1d615ee7783544cd07ee1f2c2bbb20fed6
--- /dev/null
+++ b/doc/api/pipeline_triggers.md
@@ -0,0 +1,170 @@
+# Pipeline triggers
+
+You can read more about [triggering pipelines through the API](../ci/triggers/README.md).
+
+## List project triggers
+
+Get a list of project's build triggers.
+
+```
+GET /projects/:id/triggers
+```
+
+| Attribute | Type    | required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+[
+    {
+        "id": 10,
+        "description": "my trigger",
+        "created_at": "2016-01-07T09:53:58.235Z",
+        "deleted_at": null,
+        "last_used": null,
+        "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+        "updated_at": "2016-01-07T09:53:58.235Z",
+        "owner": null
+    }
+]
+```
+
+## Get trigger details
+
+Get details of project's build trigger.
+
+```
+GET /projects/:id/triggers/:trigger_id
+```
+
+| Attribute | Type    | required | Description              |
+|-----------|---------|----------|--------------------------|
+| `id`      | integer | yes      | The ID of a project      |
+| `token`   | string  | yes      | The `token` of a trigger |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Create a project trigger
+
+Create a trigger for a project.
+
+```
+POST /projects/:id/triggers
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `id`          | integer | yes      | The ID of a project      |
+| `description` | string  | yes      | The trigger name         |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Update a project trigger
+
+Update a trigger for a project.
+
+```
+PUT /projects/:id/triggers/:trigger_id
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `trigger_id`  | integer | yes      | The trigger id           |
+| `description` | string  | no       | The trigger name         |
+
+```
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers/10"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Take ownership of a project trigger
+
+Update an owner of a project trigger.
+
+```
+POST /projects/:id/triggers/:trigger_id/take_ownership
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `trigger_id`  | integer | yes      | The trigger id           |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take_ownership"
+```
+
+```json
+{
+    "id": 10,
+    "description": "my trigger",
+    "created_at": "2016-01-07T09:53:58.235Z",
+    "deleted_at": null,
+    "last_used": null,
+    "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+    "updated_at": "2016-01-07T09:53:58.235Z",
+    "owner": null
+}
+```
+
+## Remove a project trigger
+
+Remove a project's build trigger.
+
+```
+DELETE /projects/:id/triggers/:trigger_id
+```
+
+| Attribute      | Type    | required | Description              |
+|----------------|---------|----------|--------------------------|
+| `id`           | integer | yes      | The ID of a project      |
+| `trigger_id`   | integer | yes      | The trigger id           |
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index f3c9827f7425811539942a2828f6b732618d13b0..574a8bacb25f22394fbea2ba9589358376145fcd 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -13,7 +13,7 @@ GET /projects/:id/pipelines
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines"
 ```
 
 Example of response
@@ -24,49 +24,13 @@ Example of response
     "id": 47,
     "status": "pending",
     "ref": "new-pipeline",
-    "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
-    "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
-    "tag": false,
-    "yaml_errors": null,
-    "user": {
-      "name": "Administrator",
-      "username": "root",
-      "id": 1,
-      "state": "active",
-      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "http://localhost:3000/root"
-    },
-    "created_at": "2016-08-16T10:23:19.007Z",
-    "updated_at": "2016-08-16T10:23:19.216Z",
-    "started_at": null,
-    "finished_at": null,
-    "committed_at": null,
-    "duration": null,
-    "coverage": "30.0"
+    "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a"
   },
   {
     "id": 48,
     "status": "pending",
     "ref": "new-pipeline",
-    "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
-    "before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
-    "tag": false,
-    "yaml_errors": null,
-    "user": {
-      "name": "Administrator",
-      "username": "root",
-      "id": 1,
-      "state": "active",
-      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "http://localhost:3000/root"
-    },
-    "created_at": "2016-08-16T10:23:21.184Z",
-    "updated_at": "2016-08-16T10:23:21.314Z",
-    "started_at": null,
-    "finished_at": null,
-    "committed_at": null,
-    "duration": null,
-    "coverage": null
+    "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a"
   }
 ]
 ```
@@ -85,7 +49,7 @@ GET /projects/:id/pipelines/:pipeline_id
 | `pipeline_id` | integer | yes      | The ID of a pipeline   |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46"
 ```
 
 Example of response
@@ -131,7 +95,7 @@ POST /projects/:id/pipeline
 | `ref`       | string | yes      | Reference to commit |
 
 ```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
 ```
 
 Example of response
@@ -163,7 +127,7 @@ Example of response
 }
 ```
 
-## Retry builds in a pipeline
+## Retry jobs in a pipeline
 
 > [Introduced][ce-5837] in GitLab 8.11
 
@@ -177,7 +141,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry
 | `pipeline_id` | integer | yes   | The ID of a pipeline |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry"
 ```
 
 Response:
@@ -209,7 +173,7 @@ Response:
 }
 ```
 
-## Cancel a pipelines builds
+## Cancel a pipelines jobs 
 
 > [Introduced][ce-5837] in GitLab 8.11
 
@@ -223,7 +187,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel
 | `pipeline_id` | integer | yes   | The ID of a pipeline |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel"
 ```
 
 Response:
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 404876f62370217198b0b93213b705b8737de0cc..4f6f561b83e60157f6fe1506913683ea5914f2f0 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -3,15 +3,15 @@
 ### Snippet visibility level
 
 Snippets in GitLab can be either private, internal or public.
-You can set it with the `visibility_level` field in the snippet.
+You can set it with the `visibility` field in the snippet.
 
 Constants for snippet visibility levels are:
 
-| Visibility | visibility_level | Description |
-| ---------- | ---------------- | ----------- |
-| Private    | `0`  | The snippet is visible only the snippet creator |
-| Internal   | `10` | The snippet is visible for any logged in user |
-| Public     | `20` | The snippet can be accessed without any authentication |
+| visibility | Description |
+| ---------- | ----------- |
+| `private`  | The snippet is visible only the snippet creator |
+| `internal` | The snippet is visible for any logged in user |
+| `public`   | The snippet can be accessed without any authentication |
 
 ## List snippets
 
@@ -71,7 +71,7 @@ Parameters:
 - `title` (required) - The title of a snippet
 - `file_name` (required) - The name of a snippet file
 - `code` (required) - The content of a snippet
-- `visibility_level` (required) - The snippet's visibility
+- `visibility` (required) - The snippet's visibility
 
 ## Update snippet
 
@@ -88,7 +88,7 @@ Parameters:
 - `title` (optional) - The title of a snippet
 - `file_name` (optional) - The name of a snippet file
 - `code` (optional) - The content of a snippet
-- `visibility_level` (optional) - The snippet's visibility
+- `visibility` (optional) - The snippet's visibility
 
 ## Delete snippet
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index e9ef03a0c0c610f91ac2e53123e7a6e67b36962c..686f3dba35d83b71405c1058ce7a22b505c88ac6 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -4,23 +4,23 @@
 ### Project visibility level
 
 Project in GitLab has be either private, internal or public.
-You can determine it by `visibility_level` field in project.
+You can determine it by `visibility` field in project.
 
 Constants for project visibility levels are next:
 
-* Private. `visibility_level` is `0`.
+* `private`:
   Project access must be granted explicitly for each user.
 
-* Internal. `visibility_level` is `10`.
+* `internal`:
   The project can be cloned by any logged in user.
 
-* Public. `visibility_level` is `20`.
+* `public`:
   The project can be cloned without any authentication.
 
 
 ## List projects
 
-Get a list of projects for which the authenticated user is a member.
+Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
 
 ```
 GET /projects
@@ -34,9 +34,10 @@ Parameters:
 | `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
 | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
 | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `search` | string | no | Return list of projects matching the search criteria |
 | `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
 | `owned` | boolean | no | Limit by projects owned by the current user |
+| `membership` | boolean | no | Limit by projects that the current user is a member of |
 | `starred` | boolean | no | Limit by projects starred by the current user |
 
 ```json
@@ -45,8 +46,7 @@ Parameters:
     "id": 4,
     "description": null,
     "default_branch": "master",
-    "public": false,
-    "visibility_level": 0,
+    "visibility": "private",
     "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
     "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
     "web_url": "http://example.com/diaspora/diaspora-client",
@@ -66,7 +66,7 @@ Parameters:
     "issues_enabled": true,
     "open_issues_count": 1,
     "merge_requests_enabled": true,
-    "builds_enabled": true,
+    "jobs_enabled": true,
     "wiki_enabled": true,
     "snippets_enabled": false,
     "container_registry_enabled": false,
@@ -86,9 +86,9 @@ Parameters:
     "forks_count": 0,
     "star_count": 0,
     "runners_token": "b8547b1dc37721d05889db52fa2f02",
-    "public_builds": true,
+    "public_jobs": true,
     "shared_with_groups": [],
-    "only_allow_merge_if_build_succeeds": false,
+    "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false
   },
@@ -96,8 +96,7 @@ Parameters:
     "id": 6,
     "description": null,
     "default_branch": "master",
-    "public": false,
-    "visibility_level": 0,
+    "visibility": "private",
     "ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
     "http_url_to_repo": "http://example.com/brightbox/puppet.git",
     "web_url": "http://example.com/brightbox/puppet",
@@ -117,7 +116,7 @@ Parameters:
     "issues_enabled": true,
     "open_issues_count": 1,
     "merge_requests_enabled": true,
-    "builds_enabled": true,
+    "jobs_enabled": true,
     "wiki_enabled": true,
     "snippets_enabled": false,
     "container_registry_enabled": false,
@@ -147,9 +146,9 @@ Parameters:
     "forks_count": 0,
     "star_count": 0,
     "runners_token": "b8547b1dc37721d05889db52fa2f02",
-    "public_builds": true,
+    "public_jobs": true,
     "shared_with_groups": [],
-    "only_allow_merge_if_build_succeeds": false,
+    "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false
   }
@@ -177,8 +176,7 @@ Parameters:
   "id": 3,
   "description": null,
   "default_branch": "master",
-  "public": false,
-  "visibility_level": 0,
+  "visibility": "private",
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -198,7 +196,7 @@ Parameters:
   "issues_enabled": true,
   "open_issues_count": 1,
   "merge_requests_enabled": true,
-  "builds_enabled": true,
+  "jobs_enabled": true,
   "wiki_enabled": true,
   "snippets_enabled": false,
   "container_registry_enabled": false,
@@ -228,7 +226,7 @@ Parameters:
   "forks_count": 0,
   "star_count": 0,
   "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
-  "public_builds": true,
+  "public_jobs": true,
   "shared_with_groups": [
     {
       "group_id": 4,
@@ -241,7 +239,7 @@ Parameters:
       "group_access_level": 10
     }
   ],
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false
 }
@@ -407,8 +405,6 @@ Parameters:
       },
       "created_at": "2015-12-04T10:33:56.698Z",
       "system": false,
-      "upvote": false,
-      "downvote": false,
       "noteable_id": 377,
       "noteable_type": "Issue"
     },
@@ -437,21 +433,21 @@ Parameters:
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `name` | string | yes | The name of the new project |
-| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. |
+| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). |
 | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
 | `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 |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
 | `wiki_enabled` | boolean | no | Enable wiki for this project |
 | `snippets_enabled` | boolean | no | Enable snippets for this project |
 | `container_registry_enabled` | boolean | no | Enable container registry for this project |
 | `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | String | no | See [project visibility level](#project-visibility-level) |
 | `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -476,15 +472,15 @@ Parameters:
 | `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 |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
 | `wiki_enabled` | boolean | no | Enable wiki for this project |
 | `snippets_enabled` | boolean | no | Enable snippets for this project |
 | `container_registry_enabled` | boolean | no | Enable container registry for this project |
 | `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
 | `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -508,15 +504,15 @@ Parameters:
 | `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 |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
 | `wiki_enabled` | boolean | no | Enable wiki for this project |
 | `snippets_enabled` | boolean | no | Enable snippets for this project |
 | `container_registry_enabled` | boolean | no | Enable container registry for this project |
 | `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
 | `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -551,7 +547,7 @@ Parameters:
 | `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/star"
 ```
 
 Example response:
@@ -561,8 +557,7 @@ Example response:
   "id": 3,
   "description": null,
   "default_branch": "master",
-  "public": false,
-  "visibility_level": 10,
+  "visibility": "internal",
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -577,7 +572,7 @@ Example response:
   "issues_enabled": true,
   "open_issues_count": 1,
   "merge_requests_enabled": true,
-  "builds_enabled": true,
+  "jobs_enabled": true,
   "wiki_enabled": true,
   "snippets_enabled": false,
   "container_registry_enabled": false,
@@ -596,9 +591,9 @@ Example response:
   "shared_runners_enabled": true,
   "forks_count": 0,
   "star_count": 1,
-  "public_builds": true,
+  "public_jobs": true,
   "shared_with_groups": [],
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false
 }
@@ -617,7 +612,7 @@ POST /projects/:id/unstar
 | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unstar"
 ```
 
 Example response:
@@ -627,8 +622,7 @@ Example response:
   "id": 3,
   "description": null,
   "default_branch": "master",
-  "public": false,
-  "visibility_level": 10,
+  "visibility": "internal",
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -643,7 +637,7 @@ Example response:
   "issues_enabled": true,
   "open_issues_count": 1,
   "merge_requests_enabled": true,
-  "builds_enabled": true,
+  "jobs_enabled": true,
   "wiki_enabled": true,
   "snippets_enabled": false,
   "container_registry_enabled": false,
@@ -662,9 +656,9 @@ Example response:
   "shared_runners_enabled": true,
   "forks_count": 0,
   "star_count": 0,
-  "public_builds": true,
+  "public_jobs": true,
   "shared_with_groups": [],
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false
 }
@@ -684,7 +678,7 @@ POST /projects/:id/archive
 | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/archive"
 ```
 
 Example response:
@@ -694,8 +688,7 @@ Example response:
   "id": 3,
   "description": null,
   "default_branch": "master",
-  "public": false,
-  "visibility_level": 0,
+  "visibility": "private",
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -715,7 +708,7 @@ Example response:
   "issues_enabled": true,
   "open_issues_count": 1,
   "merge_requests_enabled": true,
-  "builds_enabled": true,
+  "jobs_enabled": true,
   "wiki_enabled": true,
   "snippets_enabled": false,
   "container_registry_enabled": false,
@@ -745,9 +738,9 @@ Example response:
   "forks_count": 0,
   "star_count": 0,
   "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
-  "public_builds": true,
+  "public_jobs": true,
   "shared_with_groups": [],
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false
 }
@@ -767,7 +760,7 @@ POST /projects/:id/unarchive
 | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unarchive"
 ```
 
 Example response:
@@ -777,8 +770,7 @@ Example response:
   "id": 3,
   "description": null,
   "default_branch": "master",
-  "public": false,
-  "visibility_level": 0,
+  "visibility": "private",
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -798,7 +790,7 @@ Example response:
   "issues_enabled": true,
   "open_issues_count": 1,
   "merge_requests_enabled": true,
-  "builds_enabled": true,
+  "jobs_enabled": true,
   "wiki_enabled": true,
   "snippets_enabled": false,
   "container_registry_enabled": false,
@@ -828,9 +820,9 @@ Example response:
   "forks_count": 0,
   "star_count": 0,
   "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
-  "public_builds": true,
+  "public_jobs": true,
   "shared_with_groups": [],
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false
 }
@@ -916,7 +908,7 @@ Parameters:
 | `group_id` | integer | yes | The ID of the group |
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/share/17
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/share/17
 ```
 
 ## Hooks
@@ -963,7 +955,7 @@ Parameters:
   "merge_requests_events": true,
   "tag_push_events": true,
   "note_events": true,
-  "build_events": true,
+  "job_events": true,
   "pipeline_events": true,
   "wiki_page_events": true,
   "enable_ssl_verification": true,
@@ -990,7 +982,7 @@ Parameters:
 | `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
 | `tag_push_events` | boolean | no | Trigger hook on tag push events |
 | `note_events` | boolean | no | Trigger hook on note events |
-| `build_events` | boolean | no | Trigger hook on build events |
+| `job_events` | boolean | no | Trigger hook on job events |
 | `pipeline_events` | boolean | no | Trigger hook on pipeline events |
 | `wiki_events` | boolean | no | Trigger hook on wiki events |
 | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
@@ -1016,7 +1008,7 @@ Parameters:
 | `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
 | `tag_push_events` | boolean | no | Trigger hook on tag push events |
 | `note_events` | boolean | no | Trigger hook on note events |
-| `build_events` | boolean | no | Trigger hook on build events |
+| `job_events` | boolean | no | Trigger hook on job events |
 | `pipeline_events` | boolean | no | Trigger hook on pipeline events |
 | `wiki_events` | boolean | no | Trigger hook on wiki events |
 | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
@@ -1195,3 +1187,17 @@ Parameters:
 | `query` | string | yes | A string contained in the project name |
 | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
 | `sort` | string | no | Return requests sorted in `asc` or `desc` order |
+
+## Start the Housekeeping task for a Project
+
+>**Note:** This feature was introduced in GitLab 9.0
+
+```
+POST /projects/:id/housekeeping
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 727617f1ecc53a9c891d5e46f41203f92c563fc1..b1bf9ca07cc8b9401bcdc8b0da0c3ee103c83f3c 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -5,6 +5,8 @@
 Get a list of repository files and directories in a project. This endpoint can
 be accessed without authentication if the repository is publicly accessible.
 
+This command provides essentially the same functionality as the `git ls-tree` command. For more information, see the section _Tree Objects_ in the [Git internals documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects).
+
 ```
 GET /projects/:id/repository/tree
 ```
@@ -13,7 +15,7 @@ Parameters:
 
 - `id` (required) - The ID of a project
 - `path` (optional) - The path inside repository. Used to get contend of subdirectories
-- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
 - `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
 
 ```json
@@ -70,10 +72,11 @@ Parameters:
 ]
 ```
 
-## Raw file content
+## Get a blob from repository
 
-Get the raw file contents for a file by commit SHA and path. This endpoint can
-be accessed without authentication if the repository is publicly accessible.
+Allows you to receive information about blob in repository like size and
+content. Note that blob content is Base64 encoded. This endpoint can be accessed
+without authentication if the repository is publicly accessible.
 
 ```
 GET /projects/:id/repository/blobs/:sha
@@ -83,7 +86,6 @@ Parameters:
 
 - `id` (required) - The ID of a project
 - `sha` (required) - The commit or branch name
-- `filepath` (required) - The path the file
 
 ## Raw blob content
 
@@ -91,7 +93,7 @@ Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
 without authentication if the repository is publicly accessible.
 
 ```
-GET /projects/:id/repository/raw_blobs/:sha
+GET /projects/:id/repository/blobs/:sha/raw
 ```
 
 Parameters:
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 677e209ccd910dd4bd3b9448cc2aebeda4ae8289..aec91abd3901a4226c2534a509c53473ba65b444 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -11,11 +11,11 @@ content. Note that file content is Base64 encoded. This endpoint can be accessed
 without authentication if the repository is publicly accessible.
 
 ```
-GET /projects/:id/repository/files
+GET /projects/:id/repository/files/:file_path
 ```
 
 ```bash
-curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master'
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
 ```
 
 Example response:
@@ -36,17 +36,32 @@ Example response:
 
 Parameters:
 
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `ref` (required) - The name of branch, tag or commit
+
+## Get raw file from repository
+
+```
+GET /projects/:id/repository/files/:file_path/raw
+```
+
+```bash
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb/raw?ref=master'
+```
+
+Parameters:
+
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
 - `ref` (required) - The name of branch, tag or commit
 
 ## Create new file in repository
 
 ```
-POST /projects/:id/repository/files
+POST /projects/:id/repository/files/:file_path
 ```
 
 ```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
 ```
 
 Example response:
@@ -60,7 +75,7 @@ Example response:
 
 Parameters:
 
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
 - `branch` (required) - The name of branch
 - `encoding` (optional) - Change encoding to 'base64'. Default is text.
 - `author_email` (optional) - Specify the commit author's email address
@@ -71,11 +86,11 @@ Parameters:
 ## Update existing file in repository
 
 ```
-PUT /projects/:id/repository/files
+PUT /projects/:id/repository/files/:file_path
 ```
 
 ```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
 ```
 
 Example response:
@@ -89,7 +104,7 @@ Example response:
 
 Parameters:
 
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
 - `branch` (required) - The name of branch
 - `encoding` (optional) - Change encoding to 'base64'. Default is text.
 - `author_email` (optional) - Specify the commit author's email address
@@ -109,11 +124,11 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
 ## Delete existing file in repository
 
 ```
-DELETE /projects/:id/repository/files
+DELETE /projects/:id/repository/files/:file_path
 ```
 
 ```bash
-curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
 ```
 
 Example response:
@@ -127,7 +142,7 @@ Example response:
 
 Parameters:
 
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
 - `branch` (required) - The name of branch
 - `author_email` (optional) - Specify the commit author's email address
 - `author_name` (optional) - Specify the commit author's name
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 28610762dca875a5708030e2977b8bf1ae913b01..46f882ce9374f24281f82c2d7790cbc4e24d9032 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -18,7 +18,7 @@ GET /runners?scope=active
 | `scope`   | string  | no       | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners"
 ```
 
 Example response:
@@ -57,7 +57,7 @@ GET /runners/all?scope=online
 | `scope`   | string  | no       | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/all"
 ```
 
 Example response:
@@ -108,7 +108,7 @@ GET /runners/:id
 | `id`      | integer | yes      | The ID of a runner  |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
 ```
 
 Example response:
@@ -158,7 +158,7 @@ PUT /runners/:id
 | `tag_list`    | array   | no       | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
 
 ```
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
 ```
 
 Example response:
@@ -207,19 +207,7 @@ DELETE /runners/:id
 | `id`      | integer | yes      | The ID of a runner  |
 
 ```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
-```
-
-Example response:
-
-```json
-{
-    "active": true,
-    "description": "test-1-20150125-test",
-    "id": 6,
-    "is_shared": false,
-    "name": null,
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
 ```
 
 ## List project's runners
@@ -237,7 +225,7 @@ GET /projects/:id/runners
 | `id`      | integer | yes      | The ID of a project |
 
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners"
 ```
 
 Example response:
@@ -275,7 +263,7 @@ POST /projects/:id/runners
 | `runner_id` | integer | yes      | The ID of a runner  |
 
 ```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners" --form "runner_id=9"
 ```
 
 Example response:
@@ -306,17 +294,5 @@ DELETE /projects/:id/runners/:runner_id
 | `runner_id` | integer | yes      | The ID of a runner  |
 
 ```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
-```
-
-Example response:
-
-```json
-{
-    "active": true,
-    "description": "test-2016-02-01",
-    "id": 9,
-    "is_shared": false,
-    "name": null
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9"
 ```
diff --git a/doc/api/services.md b/doc/api/services.md
index fba5da6587d061d8dcbcc0d8b9005f3ca8e60286..7d4779f11376315623a771d2c672c5384d25d2b0 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,43 +139,6 @@ Get Buildkite service settings for a project.
 GET /projects/:id/services/buildkite
 ```
 
-## Build-Emails
-
-Get emails for GitLab CI builds.
-
-### Create/Edit Build-Emails service
-
-Set Build-Emails service for a project.
-
-```
-PUT /projects/:id/services/builds-email
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
-
-
-### Delete Build-Emails service
-
-Delete Build-Emails service for a project.
-
-```
-DELETE /projects/:id/services/builds-email
-```
-
-### Get Build-Emails service settings
-
-Get Build-Emails service settings for a project.
-
-```
-GET /projects/:id/services/builds-email
-```
-
 ## Campfire
 
 Simple web-based real-time group chat
@@ -580,8 +543,7 @@ Parameters:
 | --------- | ---- | -------- | ----------- |
 | `recipients` | string | yes | Comma-separated list of recipient email addresses |
 | `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
-
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
 
 ### Delete Pipeline-Emails service
 
@@ -810,3 +772,38 @@ GET /projects/:id/services/teamcity
 
 [jira-doc]: ../user/project/integrations/jira.md
 [old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
+
+
+## MockCI
+
+Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
+
+This service is only available when your environment is set to development.
+
+### Create/Edit MockCI service
+
+Set MockCI service for a project.
+
+```
+PUT /projects/:id/services/mock-ci
+```
+
+Parameters:
+
+- `mock_service_url` (**required**) - http://localhost:4004
+
+### Delete MockCI service
+
+Delete MockCI service for a project.
+
+```
+DELETE /projects/:id/services/mock-ci
+```
+
+### Get MockCI service settings
+
+Get MockCI service settings for a project.
+
+```
+GET /projects/:id/services/mock-ci
+```
diff --git a/doc/api/session.md b/doc/api/session.md
index d7809716fbe3d99a0f93bf83697a4bb423988aa5..056cc32597cb5f0591dd31c7443d7514675b9bed 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -21,7 +21,7 @@ POST /session
 | `password` | string  | yes     | The password of the user |
 
 ```bash
-curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
+curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
 ```
 
 Example response:
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ca6b934787733d1d8b8739fe0f40ce8de1e16c70..ad975e2e325325fd7bd15dc3baf0eb69aea042f3 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -13,14 +13,14 @@ GET /application/settings
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings
 ```
 
 Example response:
 
 ```json
 {
-   "default_projects_limit" : 10,
+   "default_projects_limit" : 100000,
    "signup_enabled" : true,
    "id" : 1,
    "default_branch_protection" : 2,
@@ -32,12 +32,13 @@ Example response:
    "updated_at" : "2016-01-04T15:44:55.176Z",
    "session_expire_delay" : 10080,
    "home_page_url" : null,
-   "default_snippet_visibility" : 0,
+   "default_snippet_visibility" : "private",
    "domain_whitelist" : [],
    "domain_blacklist_enabled" : false,
    "domain_blacklist" : [],
    "created_at" : "2016-01-04T15:44:55.176Z",
-   "default_project_visibility" : 0,
+   "default_project_visibility" : "private",
+   "default_group_visibility" : "private",
    "gravatar_enabled" : true,
    "sign_in_text" : null,
    "container_registry_token_expire_delay": 5,
@@ -59,18 +60,19 @@ PUT /application/settings
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | :------: | ----------- |
-| `default_projects_limit` | integer  | no | Project limit per user. Default is `10` |
+| `default_projects_limit` | integer  | no | Project limit per user. Default is `100000` |
 | `signup_enabled`    | boolean | no  | Enable registration. Default is `true`. |
 | `signin_enabled`    | boolean | no  | Enable login via a GitLab account. Default is `true`. |
 | `gravatar_enabled`  | boolean | no  | Enable Gravatar |
 | `sign_in_text`      | string  | no  | Text on login page |
 | `home_page_url`     | string  | no  | Redirect to this URL when not logged in |
 | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. |
-| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. |
+| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
 | `max_attachment_size` | integer | no | Limit attachment size in MB |
 | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
-| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
-| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
+| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
+| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
+| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
 | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
 | `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
 | `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
@@ -88,7 +90,7 @@ PUT /application/settings
 | `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
 ```
 
 Example response:
@@ -96,7 +98,7 @@ Example response:
 ```json
 {
   "id": 1,
-  "default_projects_limit": 10,
+  "default_projects_limit": 100000,
   "signup_enabled": true,
   "signin_enabled": true,
   "gravatar_enabled": true,
@@ -108,8 +110,9 @@ Example response:
   "restricted_visibility_levels": [],
   "max_attachment_size": 10,
   "session_expire_delay": 10080,
-  "default_project_visibility": 1,
-  "default_snippet_visibility": 0,
+  "default_project_visibility": "internal",
+  "default_snippet_visibility": "private",
+  "default_group_visibility": "private",
   "domain_whitelist": [],
   "domain_blacklist_enabled" : false,
   "domain_blacklist" : [],
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index 1ae732d40d6cf955f5dedd1db8374f25fc1ef41a..ea10a26bcd066540c7fa1b318b33f2659596f34d 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/queue_metrics
 ```
 
 Example response:
@@ -40,7 +40,7 @@ GET /sidekiq/process_metrics
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/process_metrics
 ```
 
 Example response:
@@ -82,7 +82,7 @@ GET /sidekiq/job_stats
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/job_stats
 ```
 
 Example response:
@@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/compound_metrics
 ```
 
 Example response:
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 5a5dc162ffe29f5feaa974c104db204492c30eee..e09d930698e08b3e287c4b273aa32a4735c87128 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -5,15 +5,15 @@
 ### Snippet visibility level
 
 Snippets in GitLab can be either private, internal, or public.
-You can set it with the `visibility_level` field in the snippet.
+You can set it with the `visibility` field in the snippet.
 
 Constants for snippet visibility levels are:
 
-| Visibility | Visibility level | Description |
-| ---------- | ---------------- | ----------- |
-| Private    | `0`  | The snippet is visible only to the snippet creator |
-| Internal   | `10` | The snippet is visible for any logged in user |
-| Public     | `20` | The snippet can be accessed without any authentication |
+| Visibility | Description |
+| ---------- | ----------- |
+| `private`  | The snippet is visible only to the snippet creator |
+| `internal` | The snippet is visible for any logged in user |
+| `public`   | The snippet can be accessed without any authentication |
 
 ## List snippets
 
@@ -38,7 +38,7 @@ Parameters:
 | `id`               | Integer | yes      | The ID of a snippet           |
 
 ``` bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1
 ```
 
 Example response:
@@ -78,11 +78,11 @@ Parameters:
 | `title`            | String  | yes      | The title of a snippet     |
 | `file_name`        | String  | yes      | The name of a snippet file |
 | `content`          | String  | yes      | The content of a snippet   |
-| `visibility_level` | Integer | yes      | The snippet's visibility   |
+| `visibility`       | String  | yes      | The snippet's visibility   |
 
 
 ``` bash
-curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets
 ```
 
 Example response:
@@ -123,11 +123,11 @@ Parameters:
 | `title`            | String  | no       | The title of a snippet     |
 | `file_name`        | String  | no       | The name of a snippet file |
 | `content`          | String  | no       | The content of a snippet   |
-| `visibility_level` | Integer | no       | The snippet's visibility   |
+| `visibility`       | String  | no       | The snippet's visibility   |
 
 
 ``` bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v4/snippets/1
 ```
 
 Example response:
@@ -154,7 +154,7 @@ Example response:
 
 ## Delete snippet
 
-Deletes an existing snippet. 
+Deletes an existing snippet.
 
 ```
 DELETE /snippets/:id
@@ -168,7 +168,7 @@ Parameters:
 
 
 ```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/snippets/1"
 ```
 
 upon successful delete a `204 No content` HTTP code shall be expected, with no data,
@@ -186,7 +186,7 @@ GET /snippets/public
 | `page`     | Integer | no       | the page to retrieve                  |
 
 ``` bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1
 ```
 
 Example response:
@@ -229,4 +229,3 @@ Example response:
     }
 ]
 ```
-
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 3fb8b73be6d475ff47adae4ef8774cd7c89e70d0..bad380794c1fc09b570989234c95c7c6c32bf509 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -20,7 +20,7 @@ GET /hooks
 Example request:
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks
 ```
 
 Example response:
@@ -59,7 +59,7 @@ POST /hooks
 Example request:
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/hooks?url=https://gitlab.example.com/hook"
 ```
 
 Example response:
@@ -90,7 +90,7 @@ GET /hooks/:id
 Example request:
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2
 ```
 
 Example response:
@@ -123,24 +123,5 @@ DELETE /hooks/:id
 Example request:
 
 ```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
-```
-
-Example response:
-
-```json
-{
-   "note_events" : false,
-   "project_id" : null,
-   "enable_ssl_verification" : true,
-   "url" : "https://gitlab.example.com/hook",
-   "updated_at" : "2015-11-04T20:12:15.931Z",
-   "issues_events" : false,
-   "merge_requests_events" : false,
-   "created_at" : "2015-11-04T20:12:15.931Z",
-   "service_id" : null,
-   "id" : 2,
-   "push_events" : true,
-   "tag_push_events" : false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2
 ```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 7f78ffc2390b23ea1ed2d5264cf420701f7ff689..bf350f024f525a74f80e05f849166f0d477e2532 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -26,7 +26,7 @@ Parameters:
       "committer_email": "jack@example.com",
       "id": "2695effb5807a22ff3d138d593fd856244e155e7",
       "message": "Initial commit",
-      "parents_ids": [
+      "parent_ids": [
         "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
       ]
     },
@@ -57,7 +57,7 @@ Parameters:
 | `tag_name` | string | yes | The name of the tag |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/tags/v1.0.0
 ```
 
 Example Response:
@@ -110,7 +110,7 @@ Parameters:
     "committer_email": "jack@example.com",
     "id": "2695effb5807a22ff3d138d593fd856244e155e7",
     "message": "Initial commit",
-    "parents_ids": [
+    "parent_ids": [
       "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
     ]
   },
@@ -141,11 +141,6 @@ Parameters:
 - `id` (required) - The ID of a project
 - `tag_name` (required) - The name of a tag
 
-```json
-{
-  "tag_name": "v4.3.0"
-}
-```
 
 ## Create a new release
 
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 8235be92b12df98db7c77b1d9a8dd30649f5a291..3f2f4ed54e0d2b2725b94786eb21b6d36b85ca33 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -9,7 +9,7 @@ GET /templates/gitignores
 ```
 
 ```bash
-curl https://gitlab.example.com/api/v3/templates/gitignores
+curl https://gitlab.example.com/api/v4/templates/gitignores
 ```
 
 Example response:
@@ -566,7 +566,7 @@ GET /templates/gitignores/:key
 | `key`      | string | yes      | The key of the gitignore template |
 
 ```bash
-curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby
+curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby
 ```
 
 Example response:
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
index e120016fbe6b1b17fe66bbf4f29c52e50132f2d7..27e8973da58e1204064d1bee4a05b2874eab6a97 100644
--- a/doc/api/templates/gitlab_ci_ymls.md
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -9,7 +9,7 @@ GET /templates/gitlab_ci_ymls
 ```
 
 ```bash
-curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls
+curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls
 ```
 
 Example response:
@@ -107,7 +107,7 @@ GET /templates/gitlab_ci_ymls/:key
 | `key`      | string | yes      | The key of the GitLab CI YML template |
 
 ```bash
-curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby
+curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls/Ruby
 ```
 
 Example response:
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index ae7218cf1bdf5b906b5c0e8cb78ed8e3b97dc906..33018f0c53f34e2b39c1c12b1e42b6d0e8202268 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -13,7 +13,7 @@ GET /templates/licenses
 | `popular` | boolean | no       | If passed, returns only popular licenses |
 
 ```bash
-curl https://gitlab.example.com/api/v3/templates/licenses?popular=1
+curl https://gitlab.example.com/api/v4/templates/licenses?popular=1
 ```
 
 Example response:
@@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
 the authenticated user will be used to replace the copyright holder placeholder.
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project
 ```
 
 Example response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index a2fbbc7e1f8bbef56b0d476c179a3762521951a1..77667a571955259864d26dcd914e61835eb524e7 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -22,7 +22,7 @@ Parameters:
 | `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos
 ```
 
 Example Response:
@@ -92,7 +92,7 @@ Example Response:
         "updated_at": "2016-06-17T07:47:34.163Z",
         "due_date": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_status": "cannot_be_merged",
       "subscribed": true,
       "user_notes_count": 7
@@ -165,7 +165,7 @@ Example Response:
         "updated_at": "2016-06-17T07:47:34.163Z",
         "due_date": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_status": "cannot_be_merged",
       "subscribed": true,
       "user_notes_count": 7
@@ -194,7 +194,7 @@ Parameters:
 | `id` | integer | yes | The ID of a todo |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/130/mark_as_done
 ```
 
 Example Response:
@@ -263,7 +263,7 @@ Example Response:
         "updated_at": "2016-06-17T07:47:34.163Z",
         "due_date": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_status": "cannot_be_merged",
       "subscribed": true,
       "user_notes_count": 7
@@ -284,7 +284,7 @@ POST /todos/mark_as_done
 ```
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/donmark_as_donee
 ```
 
 
diff --git a/doc/api/users.md b/doc/api/users.md
index 852c7ac8ec2cc664a8df0b1c7ab8f5f2f67cbe2f..14b5c6c713e76b1885dd62ae94b39fade993d379 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -699,7 +699,7 @@ Parameters:
 | `id` | integer | yes | The ID of the user |
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
 ```
 
 Example response:
@@ -812,8 +812,6 @@ Example response:
       },
       "created_at": "2015-12-04T10:33:56.698Z",
       "system": false,
-      "upvote": false,
-      "downvote": false,
       "noteable_id": 377,
       "noteable_type": "Issue"
     },
@@ -829,3 +827,99 @@ Example response:
   }
 ]
 ```
+
+## Retrieve user impersonation tokens
+
+It retrieves every impersonation token of the user. Note that only administrators can do this.
+This function takes pagination parameters `page` and `per_page` to restrict the list of impersonation tokens.
+
+```
+GET /users/:user_id/impersonation_tokens
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `state`   | string | no | filter tokens based on state (all, active, inactive) |
+
+Example response:
+```json
+[
+  {
+    "id": 1,
+    "name": "mytoken",
+    "revoked": false,
+    "expires_at": "2017-01-04",
+    "scopes": ['api'],
+    "active": true,
+    "impersonation": true,
+    "token": "9koXpg98eAheJpvBs5tK"
+  }
+]
+```
+
+## Show a user's impersonation token
+
+It shows a user's impersonation token. Note that only administrators can do this.
+
+```
+GET /users/:user_id/impersonation_tokens/:impersonation_token_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
+
+## Create a impersonation token
+
+It creates a new impersonation token. Note that only administrators can do this.
+You are only able to create impersonation tokens to impersonate the user and perform
+both API calls and Git reads and writes. The user will not see these tokens in his profile
+settings page.
+
+```
+POST /users/:user_id/impersonation_tokens
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `name` | string | yes | The name of the impersonation token |
+| `expires_at` | date | no | The expiration date of the impersonation token |
+| `scopes` | array | no | The array of scopes of the impersonation token (api, read_user) |
+
+Example response:
+```json
+{
+  "id": 1,
+  "name": "mytoken",
+  "revoked": false,
+  "expires_at": "2017-01-04",
+  "scopes": ['api'],
+  "active": true,
+  "impersonation": true,
+  "token": "9koXpg98eAheJpvBs5tK"
+}
+```
+
+## Revoke an impersonation token
+
+It revokes an impersonation token. Note that only administrators can revoke impersonation tokens.
+
+```
+DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 59d7f0634b2707208e61d7d820f969df14e7d7e9..7f4426ee85d0ef35bfa12b31c531671fd59a3e08 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -1,20 +1,25 @@
 # V3 to V4 version
 
-Our V4 API version is currently available as *Beta*! It means that V3
-will still be supported and remain unchanged for now, but be aware that the following
-changes are in V4:
+Since GitLab 9.0, API V4 is the preferred version to be used.
 
-### Changes
+V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
 
-- Removed `/projects/:search` (use: `/projects?search=x`)
-- `iid` filter has been removed from `projects/:id/issues`
-- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
-- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
-- Project snippets do not return deprecated field `expires_at`
-- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
-- Status 409 returned for POST `project/:id/members` when a member already exists
-- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`
-- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix)
+Below are the changes made between V3 and V4.
+
+### 8.17
+
+- Removed `GET /projects/:search` (use: `GET /projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `GET /projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `GET /projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Endpoints under `GET /projects/merge_request/:id` have been removed (use: `GET /projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723)
+- Endpoints under `GET /projects/:id/keys` have been removed (use `GET /projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+
+### 9.0
+
+- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
+- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
+- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
   - `/licences`
   - `/licences/:key`
   - `/gitignores`
@@ -23,17 +28,55 @@ changes are in V4:
   - `/gitignores/:key`
   - `/gitlab_ci_ymls/:key`
   - `/dockerfiles/:key`
-- Moved `/projects/fork/:id` to `/projects/:id/fork`
-- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done`
-- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters
-- Return pagination headers for all endpoints that return an array
-- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead
-- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)`
-- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource.
-- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR)
-- Renamed param `branch_name` to `branch` on the following endpoints
-  - POST `:id/repository/branches`
-  - POST `:id/repository/commits`
-  - POST/PUT/DELETE `:id/repository/files`
-- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response
-
+- Moved `POST /projects/fork/:id` to `POST /projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
+- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410)
+- Project filters are no longer available as `GET /projects/foo`, but as `GET /projects?foo=true` instead [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962)
+  - `GET /projects/visible` & `GET /projects/all` are consolidated into `GET /projects` and can be used with or without authorization
+  - `GET /projects/owned` moved to `GET /projects?owned=true`
+  - `GET /projects/starred` moved to `GET /projects?starred=true`
+- `GET /projects` returns all projects visible to current user, even if the user is not a member [!9674](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9674)
+  - To get projects the user is a member of, use `GET /projects?membership=true`
+- Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606)
+- Added `POST /environments/:environment_id/stop` to stop an environment [!8808](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8808)
+- Removed `DELETE /projects/:id/deploy_keys/:key_id/disable`. Use `DELETE /projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
+- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371)
+- Make subscription API more RESTful. Use `POST /projects/:id/:subscribable_type/:subscribable_id/subscribe` to subscribe and `POST /projects/:id/:subscribable_type/:subscribable_id/unsubscribe` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
+- Labels filter on `GET /projects/:id/issues` and `GET /issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
+- Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+  - `POST /projects/:id/repository/branches`
+  - `POST /projects/:id/repository/commits`
+  - `POST/PUT/DELETE :id/repository/files`
+- Renamed the `merge_when_build_succeeds` parameter to `merge_when_pipeline_succeeds` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
+  - `PUT /projects/:id/merge_requests/:merge_request_id/merge`
+  - `POST /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
+  - `POST /projects`
+  - `POST /projects/user/:user_id`
+  - `PUT /projects/:id`
+- Renamed `branch_name` to `branch` on `DELETE /projects/:id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
+- Remove `subscribed` field from responses returning list of issues or merge
+  requests. Fetch individual issues or merge requests to obtain the value
+  of `subscribed`
+  [!9661](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9661)
+- Use `visibility` as string parameter everywhere [!9337](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9337)
+- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
+- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
+- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
+- Return 202 with JSON body on async removals on V4 API (`DELETE /projects/:id/repository/merged_branches` and `DELETE /projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
+- `GET /projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
+- Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875)
+- Renamed all `build` references to `job` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Drop `GET /projects/:id/repository/commits/:sha/jobs` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713)
+  - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline`
+  - Require description when creating a new trigger `POST /projects/:id/triggers`
+- Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675)
+- API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
+- API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
+- Change initial page from `0` to `1` on `GET /projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Return correct `Link` header data for `GET /projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637)
+  - Moved `GET /projects/:id/repository/files?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+  - `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
+  - Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path`  and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
+  - `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
diff --git a/doc/api/version.md b/doc/api/version.md
index 287d17cf97f841bfdec210a1f1382867a72c040f..8b2a5b51bc55d14e53f0fea6766ccb2fead68b79 100644
--- a/doc/api/version.md
+++ b/doc/api/version.md
@@ -10,7 +10,7 @@ GET /version
 ```
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/version
 ```
 
 Example response:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index cbab7c9f18d24b5bd0f4409195f4541b73f4eaeb..d8fba5d7a778f6780664d733b0c0255c3bc43bae 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -27,6 +27,8 @@
 
 ## Breaking changes
 
+- [CI variables renaming](variables/README.md#9-0-renaming) Read about the
+  deprecated CI variables and what you should use for GitLab 9.0+.
 - [New CI job permissions model](../user/project/new_ci_build_permissions_model.md)
   Read about what changed in GitLab 8.12 and how that affects your jobs.
   There's a new way to access your Git submodules and LFS objects in jobs.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 6ae6269b28aeb63f709cb6ebbaf7e549ef9487f7..f58ab4d87af7f56f5a9ec9caec093acaa61cb198 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -298,14 +298,14 @@ could look like:
    - docker:dind
    stage: build
    script:
-     - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+     - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
      - docker build -t registry.example.com/group/project/image:latest .
      - docker push registry.example.com/group/project/image:latest
 ```
 
 You have to use the special `gitlab-ci-token` user created for you in order to
 push to the Registry connected to your project. Its password is provided in the
-`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
+`$CI_JOB_TOKEN` variable. This allows you to automate building and deployment
 of your Docker images.
 
 You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
@@ -315,10 +315,10 @@ services:
   - docker:dind
 
 variables:
-  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
+  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
 
 before_script:
-  - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+  - docker login -u gitlab-ci-token -p $CI_COMMIT_TOKEN $CI_REGISTRY
 
 build:
   stage: build
@@ -328,7 +328,7 @@ build:
 ```
 
 Here, `$CI_REGISTRY_IMAGE` would be resolved to the address of the registry tied
-to this project, and `$CI_BUILD_REF_NAME` would be resolved to the branch or
+to this project, and `$CI_COMMIT_REF_NAME` would be resolved to the branch or
 tag name for this particular job. We also declare our own variable, `$IMAGE_TAG`,
 combining the two to save us some typing in the `script` section.
 
@@ -350,11 +350,11 @@ stages:
 - deploy
 
 variables:
-  CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_BUILD_REF_NAME
+  CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
   CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
 
 before_script:
-  - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+  - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
 
 build:
   stage: build
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 00787323b6bc7e7085435e77b24598ebe1dff288..f025a7e34962591d18726fb9557465088398e038 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -170,13 +170,17 @@ services:
 ```
 
 When the job is run, `tutum/wordpress` will be started and you will have
-access to it from your build container under the hostname `tutum__wordpress`.
+access to it from your build container under the hostnames `tutum-wordpress`
+(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
 
-The alias hostname for the service is made from the image name following these
+*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
+
+The alias hostnames for the service are made from the image name following these
 rules:
 
 1. Everything after `:` is stripped
-2. Slash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
+3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
 
 ## Configuring services
 
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3c31ba45d3d243f9d026832a62613efe74b7336b..b28f3e13eaef61a21d4a8633c8de2f38ebb1b872 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -263,7 +263,7 @@ This works just like any other terminal - you'll be in the container created
 by your deployment, so you can run shell commands and get responses in real
 time, check the logs, try out configuration or code tweaks, etc. You can open
 multiple terminals to the same environment - they each get their own shell
-session -  and even a multiplexer like `screen` or `tmux`!  
+session -  and even a multiplexer like `screen` or `tmux`!
 
 >**Note:**
 Container-based deployments often lack basic tools (like an editor), and may
@@ -295,7 +295,7 @@ deploy_review:
   script:
     - echo "Deploy a review app"
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     url: https://$CI_ENVIRONMENT_SLUG.example.com
   only:
     - branches
@@ -306,22 +306,22 @@ deploy_review:
 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
+`environment:name` being `review/$CI_COMMIT_REF_NAME`. Now that's an interesting
 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. Since `$CI_BUILD_REF_NAME` itself may
+So, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`
+which takes the value of the branch name. Since `$CI_COMMIT_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
+given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
 like `https://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.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+You could also use `$CI_COMMIT_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_COMMIT_REF_SLUG.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
@@ -356,7 +356,7 @@ deploy_review:
   script:
     - echo "Deploy a review app"
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     url: https://$CI_ENVIRONMENT_SLUG.example.com
   only:
     - branches
@@ -387,16 +387,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_SLUG/public`:
+`public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
 
 ```yaml
 review_app:
   stage: deploy
   script:
-    - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
+    - rsync -av --delete public /srv/nginx/$CI_COMMIT_REF_SLUG
   environment:
-    name: review/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_SLUG.example.com
+    name: review/$CI_COMMIT_REF_NAME
+    url: https://$CI_COMMIT_REF_SLUG.example.com
 ```
 
 It is assumed that the user has already setup NGINX and GitLab Runner in the
@@ -526,7 +526,7 @@ deploy_review:
   script:
     - echo "Deploy a review app"
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     url: https://$CI_ENVIRONMENT_SLUG.example.com
     on_stop: stop_review
   only:
@@ -542,7 +542,7 @@ stop_review:
     - echo "Remove review app"
   when: manual
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     action: stop
 ```
 
@@ -568,13 +568,13 @@ You can read more in the [`.gitlab-ci.yml` reference][onstop].
 
 As we've seen in the [dynamic environments](#dynamic-environments), you can
 prepend their name with a word, then followed by a `/` and finally the branch
-name which is automatically defined by the `CI_BUILD_REF_NAME` variable.
+name which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
 
 In short, environments that are named like `type/foo` are presented under a
 group named `type`.
 
-In our minimal example, we name the environments `review/$CI_BUILD_REF_NAME`
-where `$CI_BUILD_REF_NAME` is the branch name:
+In our minimal example, we name the environments `review/$CI_COMMIT_REF_NAME`
+where `$CI_COMMIT_REF_NAME` is the branch name:
 
 ```yaml
 deploy_review:
@@ -582,7 +582,7 @@ deploy_review:
   script:
     - echo "Deploy a review app"
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
 ```
 
 In that case, if you visit the Environments page, and provided the branches
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2a5401ac13a76194c95f54748f64b6675762bff6..30f209f80eb614163a3116334089c60e1a2cd3cd 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -6,7 +6,7 @@ projects.
 
 GitLab offers a [continuous integration][ci] service. If you
 [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
-and configure your GitLab project to use a [Runner], then each merge request or
+and configure your GitLab project to use a [Runner], then each commit or
 push, triggers your CI [pipeline].
 
 The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
@@ -14,8 +14,8 @@ a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
 use all three stages; stages with no jobs are simply ignored.
 
 If everything runs OK (no non-zero return values), you'll get a nice green
-checkmark associated with the pushed commit or merge request. This makes it
-easy to see whether a merge request caused any of the tests to fail before
+checkmark associated with the commit. This makes it
+easy to see whether a commit caused any of the tests to fail before
 you even look at the code.
 
 Most projects use GitLab's CI service to run the test suite so that
@@ -207,15 +207,6 @@ you expected.
 You are also able to view the status of any commit in the various pages in
 GitLab, such as **Commits** and **Merge requests**.
 
-## Enabling build emails
-
-If you want to receive e-mail notifications about the result status of the
-jobs, you should explicitly enable the **Builds Emails** service under your
-project's settings.
-
-For more information read the
-[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
-
 ## Examples
 
 Visit the [examples README][examples] to see a list of examples using GitLab
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index c679ea4e2982c5e8e5d6afaea8ded3eed6791ffe..28c484ddbe67199336949144ab675b290542923f 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -80,7 +80,7 @@ The process of adding Review Apps in your workflow would look like:
 1. [Install][install-runner] and [configure][conf-runner] a Runner that does
    the deployment.
 1. Set up a job in `.gitlab-ci.yml` that uses the predefined
-   [predefined CI environment variable][variables] `${CI_BUILD_REF_NAME}` to
+   [predefined CI environment variable][variables] `${CI_COMMIT_REF_NAME}` to
    create dynamic environments and restrict it to run only on branches.
 1. Optionally set a job that [manually stops][manual-env] the Review Apps.
 
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 49e7ac38b267824831b53bab65c7b847cddd7d1d..befaa06e9184368d92f2eb9c9e9b1d8ff7c7834c 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,14 +30,23 @@ This is the universal solution which works with any type of executor
 ## SSH keys when using the Docker executor
 
 You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment
-to the SSH key, or the `before_script` will prompt for a passphrase.
+instructions to [generate an SSH key](../../ssh/README.md). Do not add a
+passphrase to the SSH key, or the `before_script` will prompt for it.
 
 Then, create a new **Secret Variable** in your project settings on GitLab
 following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
 and in the **Value** field paste the content of your _private_ key that you
 created earlier.
 
+It is also good practice to check the server's own public key to make sure you
+are not being targeted by a man-in-the-middle attack. To do this, add another
+variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run
+the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the
+server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If
+you need to connect to multiple servers, concatenate all the server public keys
+that you collected into the **Value** of the variable. There must be one key per
+line.
+
 Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
 Add it to the top:
 
@@ -59,6 +68,11 @@ before_script:
   # you will overwrite your user's SSH config.
   - mkdir -p ~/.ssh
   - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+  # In order to properly check the server's host key, assuming you created the
+  # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines
+  # instead.
+  # - mkdir -p ~/.ssh
+  # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
 ```
 
 As a final step, add the _public_ key from the one you created earlier to the
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 740edba1f5967f288b871afabe9ab2c12bc33c06..ccaee33dc920ca414d3fe5576ce47ac5228b233f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -36,7 +36,7 @@ it will not trigger a job.
 To trigger a job you need to send a `POST` request to GitLab's API endpoint:
 
 ```
-POST /projects/:id/trigger/builds
+POST /projects/:id/trigger/pipeline
 ```
 
 The required parameters are the trigger's `token` and the Git `ref` on which
@@ -71,7 +71,7 @@ To trigger a job from webhook of another project you need to add the following
 webhook url for Push and Tag push events:
 
 ```
-https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN
 ```
 
 > **Note**:
@@ -105,7 +105,7 @@ Using cURL you can trigger a rebuild with minimal effort, for example:
 curl --request POST \
      --form token=TOKEN \
      --form ref=master \
-     https://gitlab.example.com/api/v3/projects/9/trigger/builds
+     https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 In this case, the project with ID `9` will get rebuilt on `master` branch.
@@ -114,7 +114,7 @@ Alternatively, you can pass the `token` and `ref` arguments in the query string:
 
 ```bash
 curl --request POST \
-    "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master"
+    "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
 ```
 
 ### Triggering a job within `.gitlab-ci.yml`
@@ -128,7 +128,7 @@ need to add in project's A `.gitlab-ci.yml`:
 build_docs:
   stage: deploy
   script:
-  - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
+  - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
   only:
   - tags
 ```
@@ -187,7 +187,7 @@ curl --request POST \
   --form token=TOKEN \
   --form ref=master \
   --form "variables[UPLOAD_TO_S3]=true" \
-  https://gitlab.example.com/api/v3/projects/9/trigger/builds
+  https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 ### Using webhook to trigger job
@@ -195,7 +195,7 @@ curl --request POST \
 You can add the following webhook to another project in order to trigger a job:
 
 ```
-https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true
 ```
 
 ### Using cron to trigger nightly jobs
@@ -205,7 +205,7 @@ in conjunction with cron. The example below triggers a job on the `master`
 branch of project with ID `9` every night at `00:30`:
 
 ```bash
-30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
 ```
 
 [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 8a638ed3df8b88aedf441e8b9df311b9cab2d38b..4e9094cb0f11e19f59d4d9ce7b6acaf981b279c7 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -27,79 +27,73 @@ Some of the predefined environment variables are available only if a minimum
 version of [GitLab Runner][runner] is used. Consult the table below to find the
 version of Runner required.
 
-| Variable                | GitLab | Runner | Description |
-|-------------------------|--------|--------|-------------|
-| **CI**                  | all    | 0.4    | Mark that job is executed in CI environment |
-| **GITLAB_CI**           | all    | all    | Mark that job is executed in GitLab CI environment |
-| **CI_SERVER**           | all    | all    | Mark that job is executed in CI environment |
-| **CI_SERVER_NAME**      | all    | all    | The name of CI server that is used to coordinate jobs |
-| **CI_SERVER_VERSION**   | all    | all    | GitLab version that is used to schedule jobs |
-| **CI_SERVER_REVISION**  | all    | all    | GitLab revision that is used to schedule jobs |
-| **CI_BUILD_ID**         | all    | all    | The unique id of the current job that GitLab CI uses internally |
-| **CI_BUILD_REF**        | all    | all    | The commit revision for which project is built |
-| **CI_BUILD_TAG**        | all    | 0.5    | The commit tag name. Present only when building tags. |
-| **CI_BUILD_NAME**       | all    | 0.5    | The name of the job 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 job was [triggered] |
-| **CI_BUILD_MANUAL**     | 8.12   | all    | The flag to indicate that job was manually started |
-| **CI_BUILD_TOKEN**      | all    | 1.2    | Token used for authenticating with the GitLab Container Registry |
-| **CI_PIPELINE_ID**      | 8.10   | 0.5    | The unique id of the current pipeline that GitLab CI uses internally |
-| **CI_PROJECT_ID**       | all    | all    | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_NAME**     | 8.10   | 0.5    | The project name that is currently being built |
-| **CI_PROJECT_NAMESPACE**| 8.10   | 0.5    | The project namespace (username or groupname) that is currently being built |
-| **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 job is run |
-| **CI_ENVIRONMENT_NAME** | 8.15   | all    | The name of the environment for this job |
-| **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 job |
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15    | 1.9    | Number of attempts to download artifacts running a job |
-| **RESTORE_CACHE_ATTEMPTS** | 8.15    | 1.9    | Number of attempts to restore the cache running a job |
-| **GITLAB_USER_ID**      | 8.12   | all    | The id of the user who started the job |
-| **GITLAB_USER_EMAIL**   | 8.12   | all    | The email of the user who started the job |
-
-
-Example values:
-
-```bash
-export CI_BUILD_ID="50"
-export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
-export CI_BUILD_REF_NAME="master"
-export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
-export CI_BUILD_TAG="1.0.0"
-export CI_BUILD_NAME="spec:other"
-export CI_BUILD_STAGE="test"
-export CI_BUILD_MANUAL="true"
-export CI_BUILD_TRIGGERED="true"
-export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
-export CI_PIPELINE_ID="1000"
-export CI_PROJECT_ID="34"
-export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
-export CI_PROJECT_NAME="gitlab-ce"
-export CI_PROJECT_NAMESPACE="gitlab-org"
-export CI_PROJECT_PATH="gitlab-org/gitlab-ce"
-export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce"
-export CI_REGISTRY="registry.example.com"
-export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce"
-export CI_RUNNER_ID="10"
-export CI_RUNNER_DESCRIPTION="my runner"
-export CI_RUNNER_TAGS="docker, linux"
-export CI_SERVER="yes"
-export CI_SERVER_NAME="GitLab"
-export CI_SERVER_REVISION="70606bf"
-export CI_SERVER_VERSION="8.9.0"
-export GITLAB_USER_ID="42"
-export GITLAB_USER_EMAIL="user@example.com"
-```
+>**Note:**
+Starting with GitLab 9.0, we have deprecated some variables. Read the
+[9.0 Renaming](#9-0-renaming) section to find out their replacements. **You are
+strongly advised to use the new variables as we will remove the old ones in
+future GitLab releases.**
+
+| Variable                        | GitLab | Runner | Description |
+|-------------------------------- |--------|--------|-------------|
+| **CI**                          | all    | 0.4    | Mark that job is executed in CI environment |
+| **CI_COMMIT_REF_NAME**          | 9.0    | all    | The branch or tag name for which project is built |
+| **CI_COMMIT_REF_SLUG**          | 9.0    | all    | `$CI_COMMIT_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_COMMIT_SHA**               | 9.0    | all    | The commit revision for which project is built |
+| **CI_COMMIT_TAG**               | 9.0    | 0.5    | The commit tag name. Present only when building tags. |
+| **CI_DEBUG_TRACE**              | all    | 1.7    | Whether [debug tracing](#debug-tracing) is enabled |
+| **CI_ENVIRONMENT_NAME**         | 8.15   | all    | The name of the environment for this job |
+| **CI_ENVIRONMENT_SLUG**         | 8.15   | all    | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
+| **CI_JOB_ID**                   | 9.0    | all    | The unique id of the current job that GitLab CI uses internally |
+| **CI_JOB_MANUAL**               | 8.12   | all    | The flag to indicate that job was manually started |
+| **CI_JOB_NAME**                 | 9.0    | 0.5    | The name of the job as defined in `.gitlab-ci.yml` |
+| **CI_JOB_STAGE**                | 9.0    | 0.5    | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_JOB_TOKEN**                | 9.0    | 1.2    | Token used for authenticating with the GitLab Container Registry |
+| **CI_REPOSITORY_URL**           | 9.0    | all    | The URL to clone the Git repository |
+| **CI_RUNNER_DESCRIPTION**       | 8.10   | 0.5    | The description of the runner as saved in GitLab |
+| **CI_RUNNER_ID**                | 8.10   | 0.5    | The unique id of runner being used |
+| **CI_RUNNER_TAGS**              | 8.10   | 0.5    | The defined runner tags |
+| **CI_PIPELINE_ID**              | 8.10   | 0.5    | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PIPELINE_TRIGGERED**       | all    | all    | The flag to indicate that job was [triggered] |
+| **CI_PROJECT_DIR**              | all    | all    | The full path where the repository is cloned and where the job is run |
+| **CI_PROJECT_ID**               | all    | all    | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_NAME**             | 8.10   | 0.5    | The project name that is currently being built |
+| **CI_PROJECT_NAMESPACE**        | 8.10   | 0.5    | The project namespace (username or groupname) that is currently being built |
+| **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_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_REGISTRY_PASSWORD**        | 9.0    | all    | The password to use to push containers to the GitLab Container Registry |
+| **CI_REGISTRY_USER**            | 9.0    | all    | The username to use to push containers to the GitLab Container Registry |
+| **CI_SERVER**                   | all    | all    | Mark that job is executed in CI environment |
+| **CI_SERVER_NAME**              | all    | all    | The name of CI server that is used to coordinate jobs |
+| **CI_SERVER_REVISION**          | all    | all    | GitLab revision that is used to schedule jobs |
+| **CI_SERVER_VERSION**           | all    | all    | GitLab version that is used to schedule jobs |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS**  | 8.15   | 1.9    | Number of attempts to download artifacts running a job |
+| **GET_SOURCES_ATTEMPTS**        | 8.15   | 1.9    | Number of attempts to fetch sources running a job |
+| **GITLAB_CI**                   | all    | all    | Mark that job is executed in GitLab CI environment |
+| **GITLAB_USER_ID**              | 8.12   | all    | The id of the user who started the job |
+| **GITLAB_USER_EMAIL**           | 8.12   | all    | The email of the user who started the job |
+| **RESTORE_CACHE_ATTEMPTS**      | 8.15   | 1.9    | Number of attempts to restore the cache running a job |
+
+## 9.0 Renaming
+
+To follow conventions of naming across GitLab, and to futher move away from the
+`build` term and toward `job` CI variables have been renamed for the 9.0
+release.
+
+| 8.x name              | 9.0+ name               |
+| --------------------- |------------------------ |
+| `CI_BUILD_ID`         | `CI_JOB_ID`             |
+| `CI_BUILD_REF`        | `CI_COMMIT_SHA`         |
+| `CI_BUILD_TAG`        | `CI_COMMIT_TAG`         |
+| `CI_BUILD_REF_NAME`   | `CI_COMMIT_REF_NAME`    |
+| `CI_BUILD_REF_SLUG`   | `CI_COMMIT_REF_SLUG`    |
+| `CI_BUILD_NAME`       | `CI_JOB_NAME`           |
+| `CI_BUILD_STAGE`      | `CI_JOB_STAGE`          |
+| `CI_BUILD_REPO`       | `CI_REPOSITORY_URL`     |
+| `CI_BUILD_TRIGGERED`  | `CI_PIPELINE_TRIGGERED` |
+| `CI_BUILD_MANUAL`     | `CI_JOB_MANUAL`         |
+| `CI_BUILD_TOKEN`      | `CI_JOB_TOKEN`          |
 
 ## `.gitlab-ci.yaml` defined variables
 
@@ -131,6 +125,16 @@ job_name:
   variables: []
 ```
 
+You are able to use other variables inside your variable definition (or escape them with `$$`):
+
+```yaml
+variables:
+  LS_CMD: 'ls $FLAGS $$TMP_DIR'
+  FLAGS: '-al'
+script:
+  - 'eval $LS_CMD'  # will execute 'ls -al $TMP_DIR'
+```
+
 ## Secret variables
 
 >**Notes:**
@@ -148,7 +152,8 @@ available in the build environment. It's the recommended method to use for
 storing things like passwords, secret keys and credentials.
 
 Secret variables can be added by going to your project's
-**Settings ➔ Variables ➔ Add variable**.
+**Settings ➔ CI/CD Pipelines**, then finding the section called
+**Secret Variables**.
 
 Once you set them, they will be available for all subsequent jobs.
 
@@ -231,18 +236,18 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
 ++ CI=true
 ++ export CI_DEBUG_TRACE=false
 ++ CI_DEBUG_TRACE=false
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
 ++ export CI_PROJECT_ID=1796893
 ++ CI_PROJECT_ID=1796893
 ++ export CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace
@@ -261,20 +266,20 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
 ++ CI=true
 ++ export GITLAB_CI=true
 ++ GITLAB_CI=true
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_NAME=debug_trace
-++ CI_BUILD_NAME=debug_trace
-++ export CI_BUILD_STAGE=test
-++ CI_BUILD_STAGE=test
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_COMMIT_NAME=debug_trace
+++ CI_JOB_NAME=debug_trace
+++ export CI_JOB_STAGE=test
+++ CI_JOB_STAGE=test
 ++ export CI_SERVER_NAME=GitLab
 ++ CI_SERVER_NAME=GitLab
 ++ export CI_SERVER_VERSION=8.14.3-ee
@@ -297,8 +302,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
 ++ CI_RUNNER_ID=1337
 ++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
 ++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
-++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
-++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
+++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo'
+++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo'
 ++ export CI_REGISTRY=registry.example.com
 ++ CI_REGISTRY=registry.example.com
 ++ export CI_DEBUG_TRACE=true
@@ -341,6 +346,41 @@ job_name:
     - export
 ```
 
+Example values:
+
+```bash
+export CI_JOB_ID="50"
+export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
+export CI_COMMIT_REF_NAME="master"
+export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
+export CI_COMMIT_TAG="1.0.0"
+export CI_JOB_NAME="spec:other"
+export CI_JOB_STAGE="test"
+export CI_JOB_MANUAL="true"
+export CI_JOB_TRIGGERED="true"
+export CI_JOB_TOKEN="abcde-1234ABCD5678ef"
+export CI_PIPELINE_ID="1000"
+export CI_PROJECT_ID="34"
+export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
+export CI_PROJECT_NAME="gitlab-ce"
+export CI_PROJECT_NAMESPACE="gitlab-org"
+export CI_PROJECT_PATH="gitlab-org/gitlab-ce"
+export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce"
+export CI_REGISTRY="registry.example.com"
+export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce"
+export CI_RUNNER_ID="10"
+export CI_RUNNER_DESCRIPTION="my runner"
+export CI_RUNNER_TAGS="docker, linux"
+export CI_SERVER="yes"
+export CI_SERVER_NAME="GitLab"
+export CI_SERVER_REVISION="70606bf"
+export CI_SERVER_VERSION="8.9.0"
+export GITLAB_USER_ID="42"
+export GITLAB_USER_EMAIL="user@example.com"
+export CI_REGISTRY_USER="gitlab-ci-token"
+export CI_REGISTRY_PASSWORD="longalfanumstring"
+```
+
 [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784
 [runner]: https://docs.gitlab.com/runner/
 [triggered]: ../triggers/README.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a73598df81244b4484eafa8063d22d8f63a041b2..ad3ebd144dfe40519c4331d6db6afa438dcd4773 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -70,7 +70,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
 | image         | no | Use docker image, covered in [Use Docker](../docker/README.md) |
 | services      | no | Use docker services, covered in [Use Docker](../docker/README.md) |
 | stages        | no | Define build stages |
-| types         | no | Alias for `stages` |
+| types         | no | Alias for `stages` (deprecated) |
 | before_script | no | Define commands that run before each job's script |
 | after_script  | no | Define commands that run after each job's script |
 | variables     | no | Define build variables |
@@ -130,6 +130,8 @@ There are also two edge cases worth mentioning:
 
 ### types
 
+> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead.
+
 Alias for [stages](#stages).
 
 ### variables
@@ -151,7 +153,7 @@ thus allowing to fine tune them. Variables can be also defined on a
 [job level](#job-variables).
 
 Except for the user defined variables, there are also the ones set up by the
-Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of
+Runner itself. One example would be `CI_COMMIT_REF_NAME` which has the value of
 the branch or tag name for which project is built. Apart from the variables
 you can set in `.gitlab-ci.yml`, there are also the so called secret variables
 which can be set in GitLab's UI.
@@ -166,10 +168,11 @@ which can be set in GitLab's UI.
 cached between jobs. You can only use paths that are within the project
 workspace.
 
-**By default the caching is enabled per-job and per-branch.**
+**By default caching is enabled and shared between pipelines and jobs,
+starting from GitLab 9.0**
 
-If `cache` is defined outside the scope of the jobs, it means it is set
-globally and all jobs will use its definition.
+If `cache` is defined outside the scope of jobs, it means it is set
+globally and all jobs will use that definition.
 
 Cache all files in `binaries` and `.config`:
 
@@ -202,7 +205,7 @@ rspec:
     - binaries/
 ```
 
-Locally defined cache overwrites globally defined options. The following `rspec`
+Locally defined cache overrides globally defined options. The following `rspec`
 job will cache only `binaries/`:
 
 ```yaml
@@ -213,10 +216,15 @@ cache:
 rspec:
   script: test
   cache:
+    key: rspec
     paths:
     - binaries/
 ```
 
+Note that since cache is shared between jobs, if you're using different
+paths for different jobs, you should also set a different **cache:key**
+otherwise cache content can be overwritten.
+
 The cache is provided on a best-effort basis, so don't expect that the cache
 will be always present. For implementation details, please check GitLab Runner.
 
@@ -233,6 +241,9 @@ different jobs or even different branches.
 
 The `cache:key` variable can use any of the [predefined variables](../variables/README.md).
 
+The default key is **default** across the project, therefore everything is
+shared between each pipelines and jobs by default, starting from GitLab 9.0.
+
 ---
 
 **Example configurations**
@@ -241,7 +252,7 @@ To enable per-job caching:
 
 ```yaml
 cache:
-  key: "$CI_BUILD_NAME"
+  key: "$CI_JOB_NAME"
   untracked: true
 ```
 
@@ -249,7 +260,7 @@ To enable per-branch caching:
 
 ```yaml
 cache:
-  key: "$CI_BUILD_REF_NAME"
+  key: "$CI_COMMIT_REF_NAME"
   untracked: true
 ```
 
@@ -257,7 +268,7 @@ To enable per-job and per-branch caching:
 
 ```yaml
 cache:
-  key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME"
+  key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME"
   untracked: true
 ```
 
@@ -265,7 +276,7 @@ To enable per-branch and per-stage caching:
 
 ```yaml
 cache:
-  key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME"
+  key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME"
   untracked: true
 ```
 
@@ -274,7 +285,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
 
 ```yaml
 cache:
-  key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%"
+  key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%"
   untracked: true
 ```
 
@@ -545,13 +556,30 @@ The above script will:
 
 Manual actions are a special type of job that are not executed automatically;
 they need to be explicitly started by a user. Manual actions can be started
-from pipeline, build, environment, and deployment views. You can execute the
-same manual action multiple times.
+from pipeline, build, environment, and deployment views.
 
 An example usage of manual actions is deployment to production.
 
 Read more at the [environments documentation][env-manual].
 
+Manual actions can be either optional or blocking. Blocking manual action will
+block execution of the pipeline at stage this action is defined in. It is
+possible to resume execution of the pipeline when someone executes a blocking
+manual actions by clicking a _play_ button.
+
+When pipeline is blocked it will not be merged if Merge When Pipeline Succeeds
+is set. Blocked pipelines also do have a special status, called _manual_.
+
+Manual actions are non-blocking by default. If you want to make manual action
+blocking, it is necessary to add `allow_failure: false` to the job's definition
+in `.gitlab-ci.yml`.
+
+Optional manual actions have `allow_failure: true` set by default.
+
+**Statuses of optional actions do not contribute to overall pipeline status.**
+
+> Blocking manual actions were introduced in GitLab 9.0
+
 ### environment
 
 >
@@ -711,12 +739,12 @@ deploy as review app:
   stage: deploy
   script: make deploy
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_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/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+create the `review/$CI_COMMIT_REF_NAME` environment, where `$CI_COMMIT_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
@@ -822,7 +850,7 @@ To create an archive with a name of the current job:
 ```yaml
 job:
   artifacts:
-    name: "$CI_BUILD_NAME"
+    name: "$CI_JOB_NAME"
 ```
 
 To create an archive with a name of the current branch or tag including only
@@ -831,7 +859,7 @@ the files that are untracked by Git:
 ```yaml
 job:
    artifacts:
-     name: "$CI_BUILD_REF_NAME"
+     name: "$CI_COMMIT_REF_NAME"
      untracked: true
 ```
 
@@ -841,7 +869,7 @@ tag including only the files that are untracked by Git:
 ```yaml
 job:
   artifacts:
-    name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
+    name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
     untracked: true
 ```
 
@@ -850,7 +878,7 @@ To create an archive with a name of the current [stage](#stages) and branch name
 ```yaml
 job:
   artifacts:
-    name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
+    name: "${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}"
     untracked: true
 ```
 
@@ -862,7 +890,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
 ```yaml
 job:
   artifacts:
-    name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+    name: "%CI_JOB_STAGE%_%CI_COMMIT_REF_NAME%"
     untracked: true
 ```
 
@@ -1003,6 +1031,9 @@ job:
 
 ### coverage
 
+**Notes:**
+- [Introduced][ce-7447] in GitLab 8.17.
+
 `coverage` allows you to configure how code coverage will be extracted from the
 job output.
 
@@ -1015,7 +1046,7 @@ A simple example:
 
 ```yaml
 job1:
-  coverage: /Code coverage: \d+\.\d+/
+  coverage: '/Code coverage: \d+\.\d+/'
 ```
 
 ## Git Strategy
@@ -1361,3 +1392,4 @@ CI with various languages.
 [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
+[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
diff --git a/doc/customization/branded_page_and_email_header.md b/doc/customization/branded_page_and_email_header.md
new file mode 100644
index 0000000000000000000000000000000000000000..9a0f0b382fab7c12dedebb59bb2f9bd14f5c178c
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header.md
@@ -0,0 +1,15 @@
+# Changing the logo on the overall page and email header
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Upload the custom logo (**Header logo**) in the section **Navigation bar**.
+
+![appearance](branded_page_and_email_header/appearance.png)
+
+After saving the page, your GitLab navigation bar will contain the custom logo:
+
+![custom_brand_header](branded_page_and_email_header/custom_brand_header.png)
+
+The GitLab pipeline emails will also have the custom logo:
+
+![custom_email_header](branded_page_and_email_header/custom_email_header.png)
diff --git a/doc/customization/branded_page_and_email_header/appearance.png b/doc/customization/branded_page_and_email_header/appearance.png
new file mode 100644
index 0000000000000000000000000000000000000000..abbba6f9ac9facf1b58d2caf1c3aa09830cf0342
Binary files /dev/null and b/doc/customization/branded_page_and_email_header/appearance.png differ
diff --git a/doc/customization/branded_page_and_email_header/custom_brand_header.png b/doc/customization/branded_page_and_email_header/custom_brand_header.png
new file mode 100644
index 0000000000000000000000000000000000000000..7390f8a5e4e82cf3a5fc9f8127e3d7a17f54d764
Binary files /dev/null and b/doc/customization/branded_page_and_email_header/custom_brand_header.png differ
diff --git a/doc/customization/branded_page_and_email_header/custom_email_header.png b/doc/customization/branded_page_and_email_header/custom_email_header.png
new file mode 100644
index 0000000000000000000000000000000000000000..705698ef4a8ca046c47b1702a5a731753d63c7dd
Binary files /dev/null and b/doc/customization/branded_page_and_email_header/custom_email_header.png differ
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index c71858c6a2450b3f706002e0271fb7cccfd9250c..ce39a379a0e477e1eba4c20e20e2100f5f5da704 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -1,7 +1,7 @@
-# Generate a changelog entry
+# Changelog entries
 
-This guide contains instructions for generating a changelog entry data file, as
-well as information and history about our changelog process.
+This guide contains instructions for when and how to generate a changelog entry
+file, as well as information and history about our changelog process.
 
 ## Overview
 
@@ -19,19 +19,71 @@ author: Ozzy Osbourne
 
 The `merge_request` value is a reference to a merge request that adds this
 entry, and the `author` key is used to give attribution to community
-contributors. Both are optional.
+contributors. **Both are optional**.
 
 Community contributors and core team members are encouraged to add their name to
-the `author` field. GitLab team members should not.
-
-If you're working on the GitLab EE repository, the entry will be added to
-`changelogs/unreleased-ee/` instead.
+the `author` field. GitLab team members **should not**.
 
 [changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md
 [unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/
 [YAML]: https://en.wikipedia.org/wiki/YAML
 
-## Instructions
+## What warrants a changelog entry?
+
+- Any user-facing change **should** have a changelog entry. Example: "GitLab now
+  uses system fonts for all text."
+- A fix for a regression introduced and then fixed in the same release (i.e.,
+  fixing a bug introduced during a monthly release candidate) **should not**
+  have a changelog entry.
+- Any developer-facing change (e.g., refactoring, technical debt remediation,
+  test suite changes) **should not** have a changelog entry. Example: "Reduce
+  database records created during Cycle Analytics model spec."
+- _Any_ contribution from a community member, no matter how small, **may** have
+  a changelog entry regardless of these guidelines if the contributor wants one.
+  Example: "Fixed a typo on the search results page. (Jane Smith)"
+
+## Writing good changelog entries
+
+A good changelog entry should be descriptive and concise. It should explain the
+change to a reader who has _zero context_ about the change. If you have trouble
+making it both concise and descriptive, err on the side of descriptive.
+
+- **Bad:** Go to a project order.
+- **Good:** Show a user's starred projects at the top of the "Go to project"
+  dropdown.
+
+The first example provides no context of where the change was made, or why, or
+how it benefits the user.
+
+- **Bad:** Copy [some text] to clipboard.
+- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being
+  copied.
+
+Again, the first example is too vague and provides no context.
+
+- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and
+  builds dropdown.
+- **Good:** Fix tooltips and hover states in mini pipeline graph and builds
+  dropdown.
+
+The first example is too focused on implementation details. The user doesn't
+care that we changed CSS and HTML, they care about the _end result_ of those
+changes.
+
+- **Bad:** Strip out `nil`s in the Array of Commit objects returned from
+  `find_commits_by_message_with_elastic`
+- **Good:** Fix 500 errors caused by elasticsearch results referencing
+  garbage-collected commits
+
+The first example focuses on _how_ we fixed something, not on _what_ it fixes.
+The rewritten version clearly describes the _end benefit_ to the user (fewer 500
+errors), and _when_ (searching commits with ElasticSearch).
+
+Use your best judgement and try to put yourself in the mindset of someone
+reading the compiled changelog. Does this entry add value? Does it offer context
+about _where_ and _why_ the change was made?
+
+## How to generate a changelog entry
 
 A `bin/changelog` script is available to generate the changelog entry file
 automatically.
@@ -55,19 +107,28 @@ title: Hey DZ, I added a feature to GitLab!
 merge_request:
 author:
 ```
+If you're working on the GitLab EE repository, the entry will be added to
+`changelogs/unreleased-ee/` instead.
+
+#### Arguments
 
-### Arguments
+| Argument            | Shorthand | Purpose                                       |
+| -----------------   | --------- | --------------------------------------------- |
+| [`--amend`]         |           | Amend the previous commit                     |
+| [`--force`]         | `-f`      | Overwrite an existing entry                   |
+| [`--merge-request`] | `-m`      | Set merge request ID                          |
+| [`--dry-run`]       | `-n`      | Don't actually write anything, just print     |
+| [`--git-username`]  | `-u`      | Use Git user.name configuration as the author |
+| [`--help`]          | `-h`      | Print help message                            |
 
-| Argument          | Shorthand | Purpose                                       |
-| ----------------- | --------- | --------------------------------------------- |
-| `--amend`         |           | Amend the previous commit                     |
-| `--force`         | `-f`      | Overwrite an existing entry                   |
-| `--merge-request` | `-m`      | Merge Request ID                              |
-| `--dry-run`       | `-n`      | Don't actually write anything, just print     |
-| `--git-username`  | `-u`      | Use Git user.name configuration as the author |
-| `--help`          | `-h`      | Print help message                            |
+[`--amend`]: #-amend
+[`--force`]: #-force-or-f
+[`--merge-request`]: #-merge-request-or-m
+[`--dry-run`]: #-dry-run-or-n
+[`--git-username`]: #-git-username-or-u
+[`--help`]: #-help
 
-#### `--amend`
+##### `--amend`
 
 You can pass the **`--amend`** argument to automatically stage the generated
 file and amend it to the previous commit.
@@ -88,7 +149,7 @@ merge_request:
 author:
 ```
 
-#### `--force` or `-f`
+##### `--force` or `-f`
 
 Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
 already exists.
@@ -105,7 +166,7 @@ merge_request: 1983
 author:
 ```
 
-#### `--merge-request` or `-m`
+##### `--merge-request` or `-m`
 
 Use the **`--merge-request`** or **`-m`** argument to provide the
 `merge_request` value:
@@ -119,7 +180,7 @@ merge_request: 1983
 author:
 ```
 
-#### `--dry-run` or `-n`
+##### `--dry-run` or `-n`
 
 Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
 committing anything:
@@ -135,7 +196,7 @@ author:
 $ ls changelogs/unreleased/
 ```
 
-#### `--git-username` or `-u`
+##### `--git-username` or `-u`
 
 Use the **`--git-username`** or **`-u`** argument to automatically fill in the
 `author` value with your configured Git `user.name` value:
@@ -152,7 +213,7 @@ merge_request:
 author: Jane Doe
 ```
 
-## History and Reasoning
+### History and Reasoning
 
 Our `CHANGELOG` file was previously updated manually by each contributor that
 felt their change warranted an entry. When two merge requests added their own
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index 2f49b3564ab4939e84f61223267db2dded144510..b03216fec95880a57b054a475ffe2d5bad48abef 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -2,11 +2,12 @@
 
 This document describes what services we use for testing GitLab and GitLab CI.
 
-We currently use three CI services to test GitLab:
+We currently use four CI services to test GitLab:
 
 1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
+4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development
 
 | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
 |---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index fc948a7a116d7695d70bfbeaf64cc5b2651937b8..9bed441c1311b0bb0ed113fc8c77862206612ea8 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -444,7 +444,7 @@ Rendered example:
 
 ### cURL commands
 
-- Use `https://gitlab.example.com/api/v3/` as an endpoint.
+- Use `https://gitlab.example.com/api/v4/` as an endpoint.
 - Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
 - Always put the request first. `GET` is the default so you don't have to
   include it.
@@ -468,7 +468,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation.
 Get the details of a group:
 
 ```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org
 ```
 
 #### cURL example with parameters passed in the URL
@@ -476,7 +476,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
 Create a new project under the authenticated user's namespace:
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo"
 ```
 
 #### Post data using cURL's --data
@@ -486,7 +486,7 @@ cURL's `--data` option. The example below will create a new project `foo` under
 the authenticated user's namespace.
 
 ```bash
-curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
 ```
 
 #### Post data using JSON content
@@ -495,7 +495,7 @@ curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://g
 and double quotes.
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
 ```
 
 #### Post data using form-data
@@ -504,7 +504,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which
 properly handles data encoding:
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys
 ```
 
 The above example is run by and administrator and will add an SSH public key
@@ -518,7 +518,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
 ASCII code.
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude"
 ```
 
 Use `%2F` for slashes (`/`).
@@ -530,7 +530,7 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
 `example.net`, you would do something like this:
 
 ```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings
 ```
 
 [cURL]: http://curl.haxx.se/ "cURL website"
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index ba47998de4976b9fc79c849fefa1779f70b950a5..50105a486d071033bf7f7a38846f5ed19f5de8da 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -16,6 +16,44 @@ minification, and compression of our assets.
 [jQuery][jquery] is used throughout the application's JavaScript, with
 [Vue.js][vue] for particularly advanced, dynamic elements.
 
+### Architecture
+
+The Frontend Architect is an expert who makes high-level frontend design choices
+and decides on technical standards, including coding standards, and frameworks.
+
+When you are assigned a new feature that requires architectural design,
+make sure it is discussed with one of the Frontend Architecture Experts.
+
+This rule also applies if you plan to change the architecture of an existing feature.
+
+These decisions should be accessible to everyone, so please document it on the Merge Request.
+
+You can find the Frontend Architecture experts on the [team page][team-page].
+
+You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
+
+### Realtime
+
+When writing code for realtime features we have to keep a couple of things in mind:
+1. Do not overload the server with requests.
+1. It should feel realtime.
+
+Thus, we must strike a balance between sending requests and the feeling of realtime.
+Use the following rules when creating realtime solutions.
+
+1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
+Use that as your polling interval. This way it is easy for system administrators to change the
+polling rate.
+A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
+1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
+1. Use a common library for polling.
+1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
+Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
+1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
+controlled by the server.
+1. The backend code will most likely be using etags. You do not and should not check for status
+`304 Not Modified`. The browser will transform it for you.
+
 ### Vue
 
 For more complex frontend features, we recommend using Vue.js. It shares
@@ -50,6 +88,8 @@ Let's look into each of them:
 This is the index file of your new feature. This is where the root Vue instance
 of the new feature should be.
 
+The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
+
 Don't forget to follow [these steps.][page_specific_javascript]
 
 **A folder for Components**
@@ -70,7 +110,7 @@ You can read more about components in Vue.js site, [Component System][component-
 
 **A folder for the Store**
 
-The Store is a simple object that allows us to manage the state in a single
+The Store is a class that allows us to manage the state in a single
 source of truth.
 
 The concept we are trying to follow is better explained by Vue documentation
@@ -238,6 +278,9 @@ readability.
 See the relevant style guides for our guidelines and for information on linting:
 
 - [SCSS][scss-style-guide]
+- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
+conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
+for specific rules and patterns.
 
 ## Testing
 
@@ -270,7 +313,7 @@ When exactly one object is needed for a given task, prefer to define it as a
 `class` rather than as an object literal. Prefer also to explicitly restrict
 instantiation, unless flexibility is important (e.g. for testing).
 
-```
+```javascript
 // bad
 
 gl.MyThing = {
@@ -313,6 +356,33 @@ gl.MyThing = MyThing;
 
 ```
 
+### Manipulating the DOM in a JS Class
+
+When writing a class that needs to manipulate the DOM guarantee a container option is provided.
+This is useful when we need that class to be instantiated more than once in the same page.
+
+Bad:
+```javascript
+class Foo {
+  constructor() {
+    document.querySelector('.bar');
+  }
+}
+new Foo();
+```
+
+Good:
+```javascript
+class Foo {
+  constructor(opts) {
+    document.querySelector(`${opts.container} .bar`);
+  }
+}
+
+new Foo({ container: '.my-element' });
+```
+You can find an example of the above in this [class][container-class-example];
+
 ## Supported browsers
 
 For our currently-supported browsers, see our [requirements][requirements].
@@ -434,3 +504,8 @@ Scenario: Developer can approve merge request
 [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
 [vue-resource-repo]: https://github.com/pagekit/vue-resource
 [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
+[airbnb-js-style-guide]: https://github.com/airbnb/javascript
+[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
+[team-page]: https://about.gitlab.com/team
+[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
+[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index b8669964c84b74858fd6c285b47507ffa04d0a50..a14c0752366b854d5586984dbcd10835d4edb224 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -35,7 +35,7 @@ Using this method is in general preferred over directly calling the various
 instrumentation methods.
 
 Method instrumentation should be added in the initializer
-`config/initializers/metrics.rb`.
+`config/initializers/8_metrics.rb`.
 
 ### Examples
 
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 2d82b09f30124f12579180ffef488e0daf68a8b0..51b4b398f2ca8769721ba1fbc79dc2265cbd5907 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -50,6 +50,15 @@ Notes:
   asking a GitLab developer to do it once the merge request is merged.
 - If you branch is more than 500 commits behind `master`, the job will fail and
   you should rebase your branch upon latest `master`.
+- Code reviews for merge requests often consist of multiple iterations of
+  feedback and fixes. There is no need to update your EE MR after each
+  iteration. Instead, create an EE MR as soon as you see the
+  `rake ee_compat_check` job failing. After you receive the final acceptance
+  from a Maintainer (but before the CE MR is merged) update the EE MR.
+  This helps to identify significant conflicts sooner, but also reduces the
+  number of times you have to resolve conflicts.
+- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
+  to avoid resolving the same conflicts multiple times.
 
 ## Possible type of conflicts
 
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 8232a0a113cbdde742186e5265fe6fe1b24b6c31..2b4126b43ef1e1e09023c1bf8ddec41e909ad753 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -68,7 +68,7 @@ end
 This will end up running one query for every object to update. This code can
 easily overload a database given enough rows to update or many instances of this
 code running in parallel. This particular problem is known as the
-["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations).
+["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations). You can write a test with [QueryRecoder](query_recorder.md) to detect this and prevent regressions.
 
 In this particular case the workaround is fairly easy:
 
@@ -117,6 +117,8 @@ Post.all.includes(:author).each do |post|
 end
 ```
 
+Also consider using [QueryRecoder tests](query_recorder.md) to prevent a regression when eager loading.
+
 ## Memory Usage
 
 **Summary:** merge requests **must not** increase memory usage unless absolutely
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c1f129e576ce5dbf4802f6e735ecdb1b5482b8f2..04419650b12cae3db0caef884b6a034224a74836 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -39,6 +39,7 @@ GitLab provides built-in tools to aid the process of improving performance:
 * [Sherlock](profiling.md#sherlock)
 * [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
 * [Request Profiling](../administration/monitoring/performance/request_profiling.md)
+* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
 
 GitLab employees can use GitLab.com's performance monitoring systems located at
 <http://performance.gitlab.net>, this requires you to log in using your
diff --git a/doc/development/polling.md b/doc/development/polling.md
new file mode 100644
index 0000000000000000000000000000000000000000..a7f2962acf07575c26cbec3aad70662e4b595f1d
--- /dev/null
+++ b/doc/development/polling.md
@@ -0,0 +1,41 @@
+# Polling with ETag caching
+
+Polling for changes (repeatedly asking server if there are any new changes)
+introduces high load on a GitLab instance, because it usually requires
+executing at least a few SQL queries. This makes scaling large GitLab
+instances (like GitLab.com) very difficult so we do not allow adding new
+features that require polling and hit the database.
+
+Instead you should use polling mechanism with ETag caching in Redis.
+
+## How to use it
+
+1. Add the path of the endpoint which you want to poll to
+   `Gitlab::EtagCaching::Middleware`.
+1. Implement cache invalidation for the path of your endpoint using
+   `Gitlab::EtagCaching::Store`. Whenever a resource changes you
+   have to invalidate the ETag for the path that depends on this
+   resource.
+1. Check that the mechanism works:
+   - requests should return status code 304
+   - there should be no SQL queries logged in `log/development.log`
+
+## How it works
+
+1. Whenever a resource changes we generate a random value and store it in
+   Redis.
+1. When a client makes a request we set the `ETag` response header to the value
+   from Redis.
+1. The client caches the response (client-side caching) and sends the ETag as
+   the `If-None-Match` header with every subsequent request for the same
+   resource.
+1. If the `If-None-Match` header matches the current value in Redis we know
+   that the resource did not change so we can send 304 response immediately,
+   without querying the database at all. The client's browser will use the
+   cached response.
+1. If the `If-None-Match` header does not match the current value in Redis
+   we have to generate a new response, because the resource changed.
+
+For more information see:
+- [RFC 7232](https://tools.ietf.org/html/rfc7232)
+- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index e244ad4e881d6caae77437e8f0d515f81a33f8cc..933033a09e0a8a6000d104f573856793f1f2da58 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -25,3 +25,5 @@ starting GitLab. For example:
 
 Bullet will log query problems to both the Rails log as well as the Chrome
 console.
+
+As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
new file mode 100644
index 0000000000000000000000000000000000000000..e0127aaed4cfc498a78daeab027d40e30629ee7f
--- /dev/null
+++ b/doc/development/query_recorder.md
@@ -0,0 +1,29 @@
+# QueryRecorder
+
+QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests.
+
+> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
+
+As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
+
+## How it works
+
+This style of test works by counting the number of SQL queries executed by ActiveRecord. First a control count is taken, then you add new records to the database and rerun the count. If the number of queries has significantly increased then an `N+1` queries problem exists.
+
+```ruby
+it "avoids N+1 database queries" do
+  control_count = ActiveRecord::QueryRecorder.new { visit_some_page }.count
+  create_list(:issue, 5)
+  expect { visit_some_page }.not_to exceed_query_limit(control_count)
+end
+```
+
+As an example you might create 5 issues in between counts, which would cause the query count to increase by 5 if an N+1 problem exists.
+
+> **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible.
+
+## See also
+
+- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
+- [Performance guidelines](performance.md)
+- [Merge request performance guidelines](merge_request_performance_guidelines.md#query-counts)
\ No newline at end of file
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 9b545d7f0f1bf3f0f97d1778fd412624385796ab..5ac7b8dadeb6d406a5ca253c50445cfad2133a76 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -35,8 +35,8 @@ GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
 the command line via `bundle exec karma`.
 
 - JavaScript tests live in `spec/javascripts/`, matching the folder structure of
-  `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
-  `spec/javascripts/behaviors/autosize_spec.js.es6` file.
+  `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` has a corresponding
+  `spec/javascripts/behaviors/autosize_spec.js` file.
 - Haml fixtures required for JavaScript tests live in
   `spec/javascripts/fixtures`. They should contain the bare minimum amount of
   markup necessary for the test.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index ead79ba6a10c6f9766323b2f8468ea50d5799f4b..794c8eb6bfe766f14b68af0075de62f8086df8f5 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen
 #### Discussion
 A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
 
+## Confirmation dialogs
+
+- Destruction buttons should be clear and always say what they are destroying.
+  E.g., `Delete page` instead of just `Delete`.
+- If the copy describes another action the user can take instead of the
+  destructive one, provide a way for them to do that as a secondary button.
+- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
+  confusing when you then see the `Cancel` button.
+
 ---
 
 Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e356c99762ddf2384269b84dcc3806bc0ccc02f
Binary files /dev/null and b/doc/development/ux_guide/img/karolina-plaskaty.png differ
diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png
new file mode 100644
index 0000000000000000000000000000000000000000..01ba0391630f4b5a2ab23aa8ab3bf570d943a3dc
Binary files /dev/null and b/doc/development/ux_guide/img/nazim-ramesh.png differ
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index da410a8de7a541f94c017b506d0d966fb1b35d23..cbd7c17de412bcb5e9901ac6e96e33b07ce825c1 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -14,7 +14,7 @@
 ### Nazim Ramesh
 - Small to medium size organisations using GitLab CE
 
-<img src="img/steven-lyons.png" width="300px">
+<img src="img/nazim-ramesh.png" width="300px">
 
 #### Demographics 
 
@@ -27,19 +27,19 @@
 - **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
 
 #### Motivations
-Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab:
+Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
 
 >
 “I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.”
 >
 
-In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
+In his role as a full-stack web developer, Nazim could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Nazim recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
 
 >
 “The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.”
 >
 
-Undeterred, Steven decided to migrate a couple of projects across to GitLab. 
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab. 
 
 >
 “Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.”
@@ -47,17 +47,17 @@ Undeterred, Steven decided to migrate a couple of projects across to GitLab.
 
 Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab. 
 
-The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab. 
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab. 
 
 #### Frustrations
 ##### Adoption to GitLab has been slow
-Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
+Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
 
 ##### Missing Features
-Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
+Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
 
 ##### Regressions and bugs
-Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
+Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
 
 ##### Uses too much RAM and CPU
 >
@@ -65,7 +65,7 @@ Steven often has to calm down his colleagues, when a release contains regression
 >
 
 ##### UI/UX
-GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
+GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
 
 #### Goals 
 * To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
@@ -121,8 +121,8 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
 
 #### Goals 
 * To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
-* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily.
-* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven.
+* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
+* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
 
 <hr>
 
@@ -131,7 +131,7 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
 - Would like to use GitLab at work
 - Working for a medium to large size organisation   
 
-<img src="img/harry-robison.png" width="300px">
+<img src="img/karolina-plaskaty.png" width="300px">
 
 #### Demographics
 
@@ -144,21 +144,21 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
 - **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
 
 #### Motivations
-Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”.  He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.”  He’s also an avid reader of GitLab’s blog.
+Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”.  She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.”  She’s also an avid reader of GitLab’s blog.
 
-Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms.
+Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
 
 #### Frustrations
 ##### Unable to use GitLab at work
-Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab.
+Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
 
 ##### Performance
-GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog”  which has deterred him from running GitLab on his own machine for just hobby / personal projects.
+GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog”  which has deterred her from running GitLab on her own machine for just hobby / personal projects.
 
 ##### UX/UI
-Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this.
+Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
 
 #### Goals 
-* To develop his programming experience and to learn from other developers.
-* To contribute to both his own and other open source projects.
+* To develop her programming experience and to learn from other developers.
+* To contribute to both her own and other open source projects.
 * To use a fast and intuitive version control platform.
\ No newline at end of file
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index a6d22e5a04a299d02b840148ca7ce52de8ea9558..fe4b6d737710d47e4ea553b2ad0857259e7cd068 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,13 +15,6 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
 you should disable these mechanisms before downgrading and you should provide
 alternative authentication methods to your users.
 
-### Git Annex
-
-Git Annex is also only available on the Enterprise Edition. This means that if
-you have repositories that use Git Annex to store large files, these files will
-no longer be easily available via Git. You should consider migrating these
-repositories to use Git LFS before downgrading to the Community Edition.
-
 ### Remove Jenkins CI Service entries from the database
 
 The `JenkinsService` class is only available on the Enterprise Edition codebase,
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
index a19f0e57b56a497bd35a0f25a1b699c2c119e024..8d7a69e55ed9a458bc65db3b8d8e819becab0e19 100644
Binary files a/doc/gitlab-basics/img/create_new_project_button.png and b/doc/gitlab-basics/img/create_new_project_button.png differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 2d2fd8cb38073748189604cc36c8b2014282179e..d35709266e48d6f1e62514a757f7d270c6135892 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,9 +1,32 @@
 # Installation
 
-- [Installation](installation.md)
-- [Requirements](requirements.md)
-- [Structure](structure.md)
-- [Database MySQL](database_mysql.md)
-- [Digital Ocean and Docker](digitaloceandocker.md)
-- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
-- [All installation methods](https://about.gitlab.com/installation/)
+GitLab can be installed via various ways. Check the [installation methods][methods]
+for an overview.
+
+## Requirements
+
+Before installing GitLab, make sure to check the [requirements documentation](requirements.md)
+which includes useful information on the supported Operating Systems as well as
+the hardware requirements.
+
+## Installation methods
+
+- [Installation using the Omnibus packages](https://about.gitlab.com/downloads/) -
+  Install GitLab using our official deb/rpm repositories. This is the
+  recommended way.
+- [Installation from source](installation.md) - Install GitLab from source.
+  Useful for unsupported systems like *BSD. For an overview of the directory
+  structure, read the [structure documentation](structure.md).
+- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
+- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
+  GitLab on Google Cloud Platform using our official image.
+- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly
+  on DigitalOcean using Docker.
+
+## Database
+
+While the recommended database is PostgreSQL, we provide information to install
+GitLab using MySQL. Check the [MySQL documentation](database_mysql.md) for more
+information.
+
+[methods]: https://about.gitlab.com/installation/
diff --git a/doc/install/google-protobuf.md b/doc/install/google-protobuf.md
new file mode 100644
index 0000000000000000000000000000000000000000..a531b4519b394bc5ca064041ca2cf696dcd68fe0
--- /dev/null
+++ b/doc/install/google-protobuf.md
@@ -0,0 +1,26 @@
+# Installing a locally compiled google-protobuf gem
+
+First we must find the exact version of google-protobuf that your
+GitLab installation requires.
+
+    cd /home/git/gitlab
+
+    # Only one of the following two commands will print something. It
+    # will look like: * google-protobuf (3.2.0)
+    bundle list | grep google-protobuf
+    bundle check | grep google-protobuf
+
+Below we use `3.2.0` as an example. Replace it with the version number
+you found above.
+
+    cd /home/git/gitlab
+    sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby
+
+Finally, you can test whether google-protobuf loads correctly. The
+following should print 'OK'.
+
+    sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK'
+
+If the `gem install` command fails you may need to install developer
+tools. On Debian: `apt-get install build-essential libgmp-dev`, on
+Centos/RedHat `yum groupinstall 'Development Tools'`.
diff --git a/doc/install/google_cloud_platform/img/change_admin_passwd_email.png b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ffe14f60ff528afc086f4586780beea3b98b192
Binary files /dev/null and b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png differ
diff --git a/doc/install/google_cloud_platform/img/chrome_not_secure_page.png b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..e732066908fe0ab0372d5a7cad7e0c93ce02f59c
Binary files /dev/null and b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png differ
diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a1859da6e33091e34df793b0c345d78e84ca84a
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png differ
diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c4c870dbc9d52853ea7f614a8e6cf46076fa6c6
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png differ
diff --git a/doc/install/google_cloud_platform/img/gcp_landing.png b/doc/install/google_cloud_platform/img/gcp_landing.png
new file mode 100644
index 0000000000000000000000000000000000000000..6398d247ba0b30e474bba7fd326b751fbcaf5e0c
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gcp_landing.png differ
diff --git a/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..f492888ea4aaf8054f164e813f8cfe921fd565b8
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png differ
diff --git a/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png
new file mode 100644
index 0000000000000000000000000000000000000000..b38af3966e225426b6f72f8eee059fdacf496b6f
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_deployed_page.png b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..fef9ae45f32514ee4ac7e70be59a2deb73b40312
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png
new file mode 100644
index 0000000000000000000000000000000000000000..381c0fe48a5d30d8aa2910a5719ed2c57a9d48f6
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_launch_button.png b/doc/install/google_cloud_platform/img/gitlab_launch_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..50f66f66118f78e1ab6d318c38751ff74371b02a
Binary files /dev/null and b/doc/install/google_cloud_platform/img/gitlab_launch_button.png differ
diff --git a/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..00060841619de22b39e8ebe0d2e5b267a9897ac3
Binary files /dev/null and b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png differ
diff --git a/doc/install/google_cloud_platform/img/ssh_via_button.png b/doc/install/google_cloud_platform/img/ssh_via_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..26106f159ad57fc80182eba3dc7e313e8f6f6794
Binary files /dev/null and b/doc/install/google_cloud_platform/img/ssh_via_button.png differ
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..26506111548b526580746b81e53dfb9de45ea63f
--- /dev/null
+++ b/doc/install/google_cloud_platform/index.md
@@ -0,0 +1,168 @@
+# Installing GitLab on Google Cloud Platform
+
+![GCP landing page](img/gcp_landing.png)
+
+The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through
+the [Google Cloud Launcher][launcher] program.
+
+## Prerequisites
+
+There are only two prerequisites in order to install GitLab on GCP:
+
+1. You need to have a Google account.
+1. You need to sign up for the GCP program. If this is your first time, Google
+   gives you [$300 credit for free][freetrial] to consume over a 60-day period.
+
+Once you have performed those two steps, you can visit the
+[GCP launcher console][console] which has a list of all the things you can
+deploy on GCP.
+
+![GCP launcher console](img/gcp_launcher_console_home_page.png)
+
+The next step is to find and install GitLab.
+
+## Configuring and deploying the VM
+
+To deploy GitLab on GCP you need to follow five simple steps:
+
+1. Go to https://cloud.google.com/launcher and login with your Google credentials
+1. Search for GitLab from GitLab Inc. (not the same as Bitnami) and click on
+   the tile.
+
+    ![Search for GitLab](img/gcp_search_for_gitlab.png)
+
+1. In the next page, you can see an overview of the GitLab VM as well as some
+   estimated costs. Click the **Launch on Compute Engine** button to choose the
+   hardware and network settings.
+
+    ![Launch on Compute Engine](img/gcp_gitlab_overview.png)
+
+1. In the settings page you can choose things like the datacenter where your GitLab
+   server will be hosted, the number of CPUs and amount of RAM, the disk size
+   and type, etc. Read GitLab's [requirements documentation][req] for more
+   details on what to choose depending on your needs.
+
+    ![Deploy settings](img/new_gitlab_deployment_settings.png)
+
+1. As a last step, hit **Deploy** when ready. The process will finish in a few
+   seconds.
+
+    ![Deploy in progress](img/gcp_gitlab_being_deployed.png)
+
+
+## Visiting GitLab for the first time
+
+After a few seconds, GitLab will be successfully deployed and you should be
+able to see the IP address that Google assigned to the VM, as well as the
+credentials to the GitLab admin account.
+
+![Deploy settings](img/gitlab_deployed_page.png)
+
+1. Click on the IP under **Site address** to visit GitLab.
+1. Accept the self-signed certificate that Google automatically deployed in
+   order to securely reach GitLab's login page.
+1. Use the username and password that are present in the Google console page
+   to login into GitLab and click **Sign in**.
+
+      ![GitLab first sign in](img/gitlab_first_sign_in.png)
+
+Congratulations! GitLab is now installed and you can access it via your browser,
+but we're not done yet. There are some steps you need to take in order to have
+a fully functional GitLab installation.
+
+## Next steps
+
+These are the most important next steps to take after you installed GitLab for
+the first time.
+
+### Changing the admin password and email
+
+Google assigned a random password for the GitLab admin account and you should
+change it ASAP:
+
+1. Visit the GitLab admin page through the link in the Google console under
+   **Admin URL**.
+1. Find the Administrator user under the **Users** page and hit **Edit**.
+1. Change the email address to a real one and enter a new password.
+
+    ![Change GitLab admin password](img/change_admin_passwd_email.png)
+
+1. Hit **Save changes** for the changes to take effect.
+1. After changing the password, you will be signed out from GitLab. Use the
+   new credentials to login again.
+
+### Assigning a static IP
+
+By default, Google assigns an ephemeral IP to your instance. It is strongly
+recommended to assign a static IP if you are going to use GitLab in production
+and use a domain name as we'll see below.
+
+Read Google's documentation on how to [promote an ephemeral IP address][ip].
+
+### Using a domain name
+
+Assuming you have a domain name in your possession and you have correctly
+set up DNS to point to the static IP you configured in the previous step,
+here's how you configure GitLab to be aware of the change:
+
+1. SSH into the VM. You can easily use the **SSH** button in the Google console
+   and a new window will pop up.
+
+    ![SSH button](img/ssh_via_button.png)
+
+     In the future you might want to set up [connecting with an SSH key][ssh]
+     instead.
+
+1. Edit the config file of Omnibus GitLab using your favorite text editor:
+
+    ```
+    sudo vim /etc/gitlab/gitlab.rb
+    ```
+
+1. Set the `external_url` value to the domain name you wish GitLab to have
+   **without** `https`:
+
+    ```
+    external_url 'http://gitlab.example.com'
+    ```
+
+    We will set up HTTPS in the next step, no need to do this now.
+
+1. Reconfigure GitLab for the changes to take effect:
+
+    ```
+    sudo gitlab-ctl reconfigure
+    ```
+
+1. You can now visit GitLab using the domain name.
+
+### Configuring HTTPS with the domain name
+
+Although not needed, it's strongly recommended to secure GitLab with a TLS
+certificate. Follow the steps in the [Omnibus documentation][omni-ssl].
+
+### Configuring the email SMTP settings
+
+You need to configure the email SMTP settings correctly otherwise GitLab will
+not be able to send notification emails, like comments, and password changes.
+Check the [Omnibus documentation][omni-smtp] how to do so.
+
+## Further reading
+
+GitLab can be configured to authenticate with other OAuth providers, LDAP, SAML,
+Kerberos, etc. Here are some documents you might be interested in reading:
+
+- [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/)
+- [Integration documentation](https://docs.gitlab.com/ce/integration/)
+- [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html)
+- [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html)
+
+[console]: https://console.cloud.google.com/launcher "GCP launcher console"
+[freetrial]: https://console.cloud.google.com/freetrial "GCP free trial"
+[ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses"
+[gcp]: https://cloud.google.com/ "Google Cloud Platform"
+[launcher]: https://cloud.google.com/launcher/ "Google Cloud Launcher home page"
+[req]: ../requirements.md "GitLab hardware and software requirements"
+[ssh]: https://cloud.google.com/compute/docs/instances/connecting-to-instance "Connecting to Linux Instances"
+[omni-smtp]: https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings "Omnibus GitLab SMTP settings"
+[omni-ssl]: https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https "Omnibus GitLab enable HTTPS"
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 5ba338ba7d1d92d92db677a3a68be5cbb849eb95..177e1a9378bb7ed3f1793cfb1f1c9e874e3b039b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -155,10 +155,9 @@ page](https://golang.org/dl).
 ## 4. Node
 
 Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile
-javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to
-manage javascript dependencies. In many distros the versions provided by the
-official package repositories are out of date, so we'll need to install through
-the following commands:
+javascript assets, and yarn >= v0.17.0 to manage javascript dependencies.
+In many distros the versions provided by the official package  repositories
+are out of date, so we'll need to install through the following commands:
 
     # install node v7.x
     curl --location https://deb.nodesource.com/setup_7.x | bash -
@@ -289,9 +288,9 @@ sudo usermod -aG redis git
 ### Clone the Source
 
     # Clone GitLab repository
-    sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab
+    sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab
 
-**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
 
 ### Configure It
 
@@ -659,6 +658,12 @@ misconfigured gitlab-workhorse instance. Double-check that you've
 [installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse),
 and correctly [configured Nginx](#site-configuration).
 
+### google-protobuf "LoadError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found"
+
+This can happen on some platforms for some versions of the
+google-protobuf gem. The workaround is to [install a source-only
+version of this gem](google-protobuf.md).
+
 [RVM]: https://rvm.io/ "RVM Homepage"
 [rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub"
 [chruby]: https://github.com/postmodern/chruby "chruby on GitHub"
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3f90597ec8036e085abb86b768111d4fd3b2d7d5..7b586138f427c83de4b17c43492ff0165995128a 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -108,7 +108,7 @@ use the CI features.
 
 ## Unicorn Workers
 
-It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
+It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
 
 For most instances we recommend using: CPU cores + 1 = unicorn workers.
 So for a machine with 2 cores, 3 unicorn workers is ideal.
@@ -148,8 +148,18 @@ Sidekiq processes the background jobs with a multithreaded process.
 This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
 On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory.
 
+## Prometheus and its exporters
+
+As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related
+exporters are enabled by default, to enable easy and in depth monitoring of
+GitLab. Approximately 200MB of memory will be consumed by these processes, with
+default settings.
+
+If you would like to disable Prometheus and it's exporters or read more information
+about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md).
+
 ## Supported web browsers
 
 We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
 
-Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
\ No newline at end of file
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 22bdf33443d6497572f9267ff2b36f1c94706e96..e56e58498a6dcd19c8aec93128c714769b230ef8 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services.
 - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
 - [CAS](cas.md) Configure GitLab to sign in using CAS
 - [OAuth2 provider](oauth_provider.md) OAuth2 application creation
+- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
 - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
 - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
 - [Akismet](akismet.md) Configure Akismet to stop spam
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index 212b4854dd737e8e94c4ce6ba3cc179e5002d9eb..c39d7ab57c66e9844df6cf9b079d1cff5c0ef8e1 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -54,7 +54,7 @@ for initial settings.
       gitlab_rails['omniauth_providers'] = [
         {
           "name" => "auth0",
-          "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'',
+          "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID',
                       client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
                       namespace: 'YOUR_AUTH0_DOMAIN'
                     }
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index f8370cd349efe9df0f6921ade171fb273f52d1af..2bc526dc3dbf10a71ff94eee5f7dda91592dade9 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -1,63 +1 @@
-# Crowd OmniAuth Provider
-
-To enable the Crowd OmniAuth provider you must register your application with Crowd. To configure Crowd integration you need an application name and password.  
-
-1.  On your GitLab server, open the configuration file.
-
-    For omnibus package:
-
-    ```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](omniauth.md#initial-omniauth-configuration) for initial settings.
-
-1.  Add the provider configuration:
-
-    For omnibus package:
-
-    ```ruby
-      gitlab_rails['omniauth_providers'] = [
-        {
-          "name" => "crowd",
-          "args" => { 
-            "crowd_server_url" => "CROWD",
-            "application_name" => "YOUR_APP_NAME",
-            "application_password" => "YOUR_APP_PASSWORD"
-          }
-        }
-      ]
-    ```
-
-    For installations from source:
-
-    ```
-       - { name: 'crowd',
-           args: {
-             crowd_server_url: 'CROWD SERVER URL',
-             application_name: 'YOUR_APP_NAME',
-             application_password: 'YOUR_APP_PASSWORD' } }
-    ```
-
-1.  Change 'YOUR_APP_NAME' to the application name from Crowd applications page.
-
-1.  Change 'YOUR_APP_PASSWORD' to the application password you've set.
-
-1.  Save the configuration file.
-
-1.  [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
-    installed GitLab via Omnibus or from source respectively.
-
-On the sign in page there should now be a Crowd tab in the sign in form.
-
-[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
-
+This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md).
diff --git a/doc/integration/github.md b/doc/integration/github.md
index cea85f073ccc5889a3a02a04bad96b6c8fa88845..4b0d33334bd31e20b60868fce2984941627693a4 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use.
     - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
     - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
     - Application description: Fill this in if you wish.
-    - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
+    - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port.
 1.  Select "Register application".
 
 1.  You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 47e20d7566a5567029a56085ee1be0b8c5145fff..6c11f46a70aa23325c3a02201acd118f8f4aae90 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -27,7 +27,7 @@ contains some settings that are common for all providers.
 - [Twitter](twitter.md)
 - [Shibboleth](shibboleth.md)
 - [SAML](saml.md)
-- [Crowd](crowd.md)
+- [Crowd](../administration/auth/crowd.md)
 - [Azure](azure.md)
 - [Auth0](auth0.md)
 - [Authentiq](../administration/auth/authentiq.md)
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
new file mode 100644
index 0000000000000000000000000000000000000000..56f367d841ef287c7298982901724733324395ff
--- /dev/null
+++ b/doc/integration/openid_connect_provider.md
@@ -0,0 +1,47 @@
+# GitLab as OpenID Connect identity provider
+
+This document is about using GitLab as an OpenID Connect identity provider
+to sign in to other services.
+
+## Introduction to OpenID Connect
+
+[OpenID Connect] \(OIC) is a simple identity layer on top of the
+OAuth 2.0 protocol. It allows clients to verify the identity of the end-user
+based on the authentication performed by GitLab, as well as to obtain
+basic profile information about the end-user in an interoperable and
+REST-like manner. OIC performs many of the same tasks as OpenID 2.0,
+but does so in a way that is API-friendly, and usable by native and
+mobile applications.
+
+On the client side, you can use [omniauth-openid-connect] for Rails
+applications, or any of the other available [client implementations].
+
+GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer
+to its README for more details about which parts of the specifications
+are supported.
+
+## Enabling OpenID Connect for OAuth applications
+
+Refer to the [OAuth guide] for basic information on how to set up OAuth
+applications in GitLab. To enable OIC for an application, all you have to do
+is select the `openid` scope in the application settings.
+
+Currently the following user information is shared with clients:
+
+| Claim            | Type      | Description |
+|:-----------------|:----------|:------------|
+| `sub`            | `string`  | An opaque token that uniquely identifies the user
+| `auth_time`      | `integer` | The timestamp for the user's last authentication
+| `name`           | `string`  | The user's full name
+| `nickname`       | `string`  | The user's GitLab username
+| `email`          | `string`  | The user's public email address
+| `email_verified` | `boolean` | Whether the user's public email address was verified
+| `website`        | `string`  | URL for the user's website
+| `profile`        | `string`  | URL for the user's GitLab profile
+| `picture`        | `string`  | URL for the user's GitLab avatar
+
+[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website"
+[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website"
+[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider"
+[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website"
+[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations"
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 7a809eddac0804227c5c6be904ae274ab3ecde5f..2277aa827b7f9ca4952737348beeede668ff2d82 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -74,7 +74,7 @@ in your SAML IdP:
                    idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
                    idp_sso_target_url: 'https://login.example.com/idp',
                    issuer: 'https://gitlab.example.com',
-                   name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+                   name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
                  },
           label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
         }
@@ -91,7 +91,7 @@ in your SAML IdP:
                  idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
                  idp_sso_target_url: 'https://login.example.com/idp',
                  issuer: 'https://gitlab.example.com',
-                 name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+                 name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
                },
           label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
         }
@@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element:
           idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
           idp_sso_target_url: 'https://login.example.com/idp',
           issuer: 'https://gitlab.example.com',
-          name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+          name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
         } }
 ```
 
@@ -227,7 +227,7 @@ args: {
         idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
         idp_sso_target_url: 'https://login.example.com/idp',
         issuer: 'https://gitlab.example.com',
-        name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+        name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
         attribute_statements: { email: ['EmailAddress'] }
 }
 ```
@@ -245,7 +245,7 @@ args: {
         idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
         idp_sso_target_url: 'https://login.example.com/idp',
         issuer: 'https://gitlab.example.com',
-        name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+        name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
         attribute_statements: { email: ['EmailAddress'] },
         allowed_clock_drift: 1 # for one second clock drift
 }
diff --git a/doc/pages/README.md b/doc/pages/README.md
index c9715eed5988854ffa691b467aacbcc6df933acb..7878bce3f1022e9130c3bce6ed103f7b5895501a 100644
--- a/doc/pages/README.md
+++ b/doc/pages/README.md
@@ -1 +1 @@
-This document was moved to [user/project/pages](../user/project/pages/index.md).
+This document was moved to [pages/index.md](../user/project/pages/index.md).
diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md
new file mode 100644
index 0000000000000000000000000000000000000000..1d63ccb4d2ff9160f532d68915f4dd77d2e073b8
--- /dev/null
+++ b/doc/pages/getting_started_part_one.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_one.md).
diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md
new file mode 100644
index 0000000000000000000000000000000000000000..1697b5cd6b4c5586370ed4f0b559caa0ac7e2860
--- /dev/null
+++ b/doc/pages/getting_started_part_three.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_three.md).
diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md
new file mode 100644
index 0000000000000000000000000000000000000000..a58affec73dd5a3b93561b3bb970e0f9e76a111b
--- /dev/null
+++ b/doc/pages/getting_started_part_two.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_two.md).
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
deleted file mode 100644
index ee54d8652256619150abeda05eea1233610af1c3..0000000000000000000000000000000000000000
--- a/doc/project_services/builds_emails.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index a5b8cd6455cd95118d63aa4ace1d100b85da4a7e..65fcfc77ab17887c58c1d75b0b535c2d6cc0bbed 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -38,23 +38,6 @@ If you are running GitLab within a Docker container, you can run the backup from
 docker exec -t <container name> gitlab-rake gitlab:backup:create
 ```
 
-You can specify that portions of the application data be skipped using the
-environment variable `SKIP`. You can skip:
-
-- `db` (database)
-- `uploads` (attachments)
-- `repositories` (Git repositories data)
-- `builds` (CI job output logs)
-- `artifacts` (CI job artifacts)
-- `lfs` (LFS objects)
-- `registry` (Container Registry images)
-
-Separate multiple data types to skip using a comma. For example:
-
-```
-sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
-```
-
 Example output:
 
 ```
@@ -111,13 +94,14 @@ To use the `copy` strategy instead of the default streaming strategy, specify
 You can choose what should be backed up by adding the environment variable `SKIP`.
 The available options are:
 
-* `db`
-* `uploads` (attachments)
-* `repositories`
-* `builds` (CI build output logs)
-* `artifacts` (CI build artifacts)
-* `lfs` (LFS objects)
-* `pages` (pages content)
+- `db` (database)
+- `uploads` (attachments)
+- `repositories` (Git repositories data)
+- `builds` (CI job output logs)
+- `artifacts` (CI job artifacts)
+- `lfs` (LFS objects)
+- `registry` (Container Registry images)
+- `pages` (Pages content)
 
 Use a comma to specify several options at the same time:
 
@@ -175,6 +159,8 @@ For installations from source:
       remote_directory: 'my.s3.bucket'
       # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
       # encryption: 'AES256'
+      # Specifies Amazon S3 storage class to use for backups, this is optional
+      # storage_class: 'STANDARD'
 ```
 
 If you are uploading your backups to S3 you will probably want to create a new
@@ -416,7 +402,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true
 
 If there is a GitLab version mismatch between your backup tar file and the installed
 version of GitLab, the restore command will abort with an error. Install the
-[correct GitLab version](https://www.gitlab.com/downloads/archives/) and try again.
+[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again.
 
 ## Configure cron to make daily backups
 
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9e391d647a86494b93cf7be3d21e49301f319974..cf28f1a2ecabbf41039ddcc5c373208a40c76e29 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -13,7 +13,7 @@ read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community
 
 ## Locating an existing SSH key pair
 
-Before generating a new SSH key check if your system already has one
+Before generating a new SSH key pair check if your system already has one
 at the default location by opening a shell, or Command Prompt on Windows,
 and running the following command:
 
@@ -23,43 +23,49 @@ and running the following command:
 type %userprofile%\.ssh\id_rsa.pub
 ```
 
-**GNU/Linux / macOS / PowerShell:**
+**Git Bash on Windows / GNU/Linux / macOS / PowerShell:**
 
 ```bash
 cat ~/.ssh/id_rsa.pub
 ```
 
 If you see a string starting with `ssh-rsa` you already have an SSH key pair
-and you can skip the next step **Generating a new SSH key pair**
-and continue onto **Copying your public SSH key to the clipboard**.
+and you can skip the generate portion of the next section and skip to the copy
+to clipboard step.
 If you don't see the string or would like to generate a SSH key pair with a
 custom name continue onto the next step.
 
+>
+**Note:** Public SSH key may also be named as follows:
+- `id_dsa.pub`
+- `id_ecdsa.pub`
+- `id_ed25519.pub`
+
 ## Generating a new SSH key pair
 
-1. To generate a new SSH key, use the following command:
+1. To generate a new SSH key pair, use the following command:
 
-    **GNU/Linux / macOS:**
+    **Git Bash on Windows / GNU/Linux / macOS:**
 
     ```bash
-    ssh-keygen -t rsa -C "GitLab" -b 4096
+    ssh-keygen -t rsa -C "your.email@example.com" -b 4096
     ```
 
     **Windows:**
 
-    On Windows you will need to download
+    Alternatively on Windows you can download
     [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
-    and follow this [documentation article][winputty] to generate a SSH key pair.
+    and follow [this documentation article][winputty] to generate a SSH key pair.
 
-1. Next, you will be prompted to input a file path to save your key pair to.
+1. Next, you will be prompted to input a file path to save your SSH key pair to.
 
     If you don't already have an SSH key pair use the suggested path by pressing
-    enter. Using the suggested path will allow your SSH client
-    to automatically use the key pair with no additional configuration.
+    enter. Using the suggested path will normally allow your SSH client
+    to automatically use the SSH key pair with no additional configuration.
 
-    If you already have a key pair with the suggested file path, you will need
-    to input a new file path and declare what host this key pair will be used
-    for in your `.ssh/config` file, see **Working with non-default SSH key pair paths**
+    If you already have a SSH key pair with the suggested file path, you will need
+    to input a new file path and declare what host this SSH key pair will be used
+    for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths)
     for more information.
 
 1. Once you have input a file path you will be prompted to input a password to
@@ -68,12 +74,12 @@ custom name continue onto the next step.
    pressing enter.
 
      >**Note:**
-     If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`.
+     If you want to change the password of your SSH key pair, you can use
+     `ssh-keygen -p <keyname>`.
 
-1. The next step is to copy the public key as we will need it afterwards.
+1. The next step is to copy the public SSH key as we will need it afterwards.
 
-    To copy your public key to the clipboard, use the appropriate code for your
-    operating system below:
+    To copy your public SSH key to the clipboard, use the appropriate code below:
 
     **macOS:**
 
@@ -93,7 +99,7 @@ custom name continue onto the next step.
     type %userprofile%\.ssh\id_rsa.pub | clip
     ```
 
-    **Windows PowerShell:**
+    **Git Bash on Windows / Windows PowerShell:**
 
     ```bash
     cat ~/.ssh/id_rsa.pub | clip
@@ -101,22 +107,38 @@ custom name continue onto the next step.
 
 1. The final step is to add your public SSH key to GitLab.
 
-    Navigate to the 'SSH Keys' tab in you 'Profile Settings'.
+    Navigate to the 'SSH Keys' tab in your 'Profile Settings'.
     Paste your key in the 'Key' section and give it a relevant 'Title'.
     Use an identifiable title like 'Work Laptop - Windows 7' or
     'Home MacBook Pro 15'.
 
     If you manually copied your public SSH key make sure you copied the entire
     key starting with `ssh-rsa` and ending with your email.
+    
+1. Optionally you can test your setup by running `ssh -T git@example.com`
+   (replacing `example.com` with your GitLab domain) and verifying that you
+   receive a `Welcome to GitLab` message.
 
 ## Working with non-default SSH key pair paths
 
 If you used a non-default file path for your GitLab SSH key pair,
-you must configure your SSH client to find your GitLab SSH private key
-for connections to your GitLab server (perhaps gitlab.com).
+you must configure your SSH client to find your GitLab private SSH key
+for connections to your GitLab server (perhaps `gitlab.com`).
+
+For your current terminal session you can do so using the following commands
+(replacing `other_id_rsa` with your private SSH key):
 
-For OpenSSH clients this is configured in the `~/.ssh/config` file.
-Below are two example host configurations using their own key:
+**Git Bash on Windows / GNU/Linux / macOS:**
+
+```bash
+eval $(ssh-agent -s)
+ssh-add ~/.ssh/other_id_rsa
+```
+
+To retain these settings you'll need to save them to a configuration file.
+For OpenSSH clients this is configured in the `~/.ssh/config` file for some
+operating systems.
+Below are two example host configurations using their own SSH key:
 
 ```
 # GitLab.com server
@@ -140,19 +162,20 @@ That's why it needs to uniquely map to a single user.
 
 ## Deploy keys
 
-Deploy keys allow read-only access to multiple projects with a single SSH
-key.
+Deploy keys allow read-only or read-write (if enabled) access to one or
+multiple projects with a single SSH key pair.
 
 This is really useful for cloning repositories to your Continuous
 Integration (CI) server. By using deploy keys, you don't have to setup a
 dummy user account.
 
 If you are a project master or owner, you can add a deploy key in the
-project settings under the section 'Deploy Keys'. Press the 'New Deploy
-Key' button and upload a public SSH key. After this, the machine that uses
-the corresponding private key has read-only access to the project.
+project settings under the section 'Repository'. Specify a title for the new
+deploy key and paste a public SSH key. After this, the machine that uses
+the corresponding private SSH key has read-only or read-write (if enabled) 
+access to the project.
 
-You can't add the same deploy key twice with the 'New Deploy Key' option.
+You can't add the same deploy key twice using the form.
 If you want to add the same key to another project, please enable it in the
 list that says 'Deploy keys from projects available to you'. All the deploy
 keys of all the projects you have access to are available. This project
@@ -166,6 +189,18 @@ project.
 
 ### Eclipse
 
-How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
+How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
 
 [winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
+
+## Troubleshooting
+
+If on Git clone you are prompted for a password like `git@gitlab.com's password:`
+something is wrong with your SSH setup.
+
+- Ensure that you generated your SSH key pair correctly and added the public SSH
+  key to your GitLab profile
+- Try manually registering your private SSH key using `ssh-agent` as documented 
+  earlier in this document
+- Try to debug the connection by running `ssh -Tv git@example.com`
+  (replacing `example.com` with your GitLab domain)
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index ec13c2446efddd38d5abc360d3f281180a035989..ad5ffc8447377fb730329e18f3314413f9a3eab9 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -313,8 +313,19 @@ X-Gitlab-Event: System Hook
     "git_ssh_url":"git@example.com:mike/diaspora.git",
     "visibility_level":0
   },
-  "commits": [],
-  "total_commits_count": 0
+  "commits": [
+    {
+      "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+      "message": "Add simple search to projects in public area",
+      "timestamp": "2013-05-13T18:18:08+00:00",
+      "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+      "author": {
+        "name": "Dmitriy Zaporozhets",
+        "email": "dmitriy.zaporozhets@gmail.com"
+      }
+    }
+  ],
+  "total_commits_count": 1
 }
 ```
 
diff --git a/doc/university/README.md b/doc/university/README.md
index 8d4e7eff1150ee1836f628a46f05c93b65884848..c1661f0b52ba2914f9b049e12bad5e92d2e7ef3f 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -165,7 +165,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
 
 #### 3.4. Large Files
 
-1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+1. [Big files in Git (Git LFS) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
 
 #### 3.5. LDAP and Active Directory
 
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index ca538ef6dc327fb59c3b582fc629024db7ad86b7..567dadb3b47dcbe41b4aba3382eb351d1492f83e 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -167,7 +167,6 @@ Some tickets need specific knowledge or a deep understanding of a particular com
 
 Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
 
-- Set up and try [Git Annex](https://docs.gitlab.com/ee/workflow/git_annex.html)
 - Set up and try [Git LFS](https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
 - Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
 - Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 75956aeb360af2687504f9827fefe59aa47b8df3..ed0e668d854f8f8c5489a83958cc9bf96d3a4832 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.6
+sudo -u git -H git checkout v3.6.7
 ```
 
 ### 6. Update gitlab-workhorse
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 954109ba18f148b3f270116c280142994b1b01d0..74ffe0bc846e6a75419d50cd13f0afe35ee48fcd 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -139,7 +139,7 @@ sudo -u git -H git checkout v4.1.1
 
 #### New configuration options for `gitlab.yml`
 
-There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+There might be new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
 
 ```sh
 cd /home/git/gitlab
@@ -195,6 +195,16 @@ See [smtp_settings.rb.sample] as an example.
 
 #### Init script
 
+There might be new configuration options available for [`gitlab.default.example`][gl-example].
+You need to update this file if you want to [enable GitLab Pages][pages-admin].
+View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:lib/support/init.d/gitlab.default.example origin/8-17-stable:lib/support/init.d/gitlab.default.example
+```
+
 Ensure you're still up-to-date with the latest init script changes:
 
 ```bash
@@ -254,3 +264,5 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
 If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
 
 [yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example
+[pages-admin]: ../administration/pages/source.md
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
new file mode 100644
index 0000000000000000000000000000000000000000..b7ba970031c4758deedcddc1d3f016e83dad9d1e
--- /dev/null
+++ b/doc/update/8.17-to-9.0.md
@@ -0,0 +1,330 @@
+# From 8.17 to 9.0
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version.  You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --location https://yarnpkg.com/install.sh | bash -
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable-ee
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 7. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 8. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v5.0.0
+```
+
+### 9. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example
+```
+
+#### Configuration changes for repository storages
+
+This version introduces a new configuration structure for repository storages.
+Update your current configuration as follows, replacing with your storages names and paths:
+
+**For installations from source**
+
+1. Update your `gitlab.yml`, from
+
+  ```yaml
+  repositories:
+    storages: # You must have at least a 'default' storage path.
+      default: /home/git/repositories
+      nfs: /mnt/nfs/repositories
+      cephfs: /mnt/cephfs/repositories
+  ```
+
+  to
+
+  ```yaml
+  repositories:
+    storages: # You must have at least a 'default' storage path.
+      default:
+        path: /home/git/repositories
+      nfs:
+        path: /mnt/nfs/repositories
+      cephfs:
+        path: /mnt/cephfs/repositories
+  ```
+
+**For Omnibus installations**
+
+1. Update your `/etc/gitlab/gitlab.rb`, from
+
+  ```ruby
+  git_data_dirs({
+    "default" => "/var/opt/gitlab/git-data",
+    "nfs" => "/mnt/nfs/git-data",
+    "cephfs" => "/mnt/cephfs/git-data"
+  })
+  ```
+
+  to
+
+  ```ruby
+  git_data_dirs({
+    "default" => { "path" => "/var/opt/gitlab/git-data" },
+    "nfs" => { "path" => "/mnt/nfs/git-data" },
+    "cephfs" => { "path" => "/mnt/cephfs/git-data" }
+  })
+  ```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-17-stable:lib/support/nginx/gitlab-ssl origin/9-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-17-stable:lib/support/nginx/gitlab origin/9-0-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:lib/support/init.d/gitlab.default.example origin/9-0-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 10. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 11. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.17)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index b8d24cb2d3b6b916a592b82dfe018fdec1bdec71..eb6f915f3f44eec5c2331e03bf91f97b03e78c00 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -3,18 +3,36 @@
 ## Maximum artifacts size
 
 The maximum size of the [job artifacts][art-yml] can be set in the Admin area
-of your GitLab instance. The value is in MB and the default is 100MB. Note that
-this setting is set for each job.
+of your GitLab instance. The value is in *MB* and the default is 100MB. Note
+that this setting is set for each job.
 
 1. Go to **Admin area > Settings** (`/admin/application_settings`).
 
     ![Admin area settings button](img/admin_area_settings_button.png)
 
-1. Change the value of the maximum artifacts size (in MB):
+1. Change the value of maximum artifacts size (in MB):
 
     ![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png)
 
 1. Hit **Save** for the changes to take effect.
 
+## Default artifacts expiration
+
+The default expiration time of the [job artifacts][art-yml] can be set in
+the Admin area of your GitLab instance. The syntax of duration is described
+in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that
+this setting is set for each job. Set it to 0 if you don't want default
+expiration.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+
+    ![Admin area settings button](img/admin_area_settings_button.png)
+
+1. Change the value of default expiration time ([syntax][duration-syntax]):
+
+    ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png)
+
+1. Hit **Save** for the changes to take effect.
 
 [art-yml]: ../../../administration/job_artifacts.md
+[duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in
diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
new file mode 100644
index 0000000000000000000000000000000000000000..50a86ede56ba0a2ca1268bdd1e748c84f51e8c7a
Binary files /dev/null and b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png differ
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
index b7d6671902ad980d5eb7497626fffa08d3fe68e0..33fd29e2039a20adb0f2e49b3c7313fe4b957e87 100644
Binary files a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png and b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png differ
diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md
new file mode 100644
index 0000000000000000000000000000000000000000..acbd2a66d372300bf2d4a00b1bbc8782b05ce39b
--- /dev/null
+++ b/doc/user/award_emojis.md
@@ -0,0 +1,51 @@
+# Award emoji
+
+>**Notes:**
+- First [introduced][1825] in GitLab 8.2.
+- GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform
+  supports them and falls back to images or CSS sprites. This change greatly
+  improved the award emoji performance overall.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and
+virtually everywhere where you can have a discussion.
+
+![Award emoji](img/award_emoji_select.png)
+
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
+
+## Sort issues and merge requests on vote count
+
+> [Introduced][2871] in GitLab 8.5.
+
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+> [Introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown. If you want to remove an award emoji, just click
+the emoji again and the vote will be removed.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570
diff --git a/doc/user/group/subgroups/img/create_new_group.png b/doc/user/group/subgroups/img/create_new_group.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d011ec709aad3296b0dd4c35061ed4ec675e318
Binary files /dev/null and b/doc/user/group/subgroups/img/create_new_group.png differ
diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..000b54c2855e972734a8a7e80164898072b1f642
Binary files /dev/null and b/doc/user/group/subgroups/img/create_subgroup_button.png differ
diff --git a/doc/user/group/subgroups/img/group_members.png b/doc/user/group/subgroups/img/group_members.png
new file mode 100644
index 0000000000000000000000000000000000000000..b95fe6263bfb4edf2d294f6f394902270aa7c9ef
Binary files /dev/null and b/doc/user/group/subgroups/img/group_members.png differ
diff --git a/doc/user/group/subgroups/img/mention_subgroups.png b/doc/user/group/subgroups/img/mention_subgroups.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e6bed0111b6a7a43247d1ea3dc5ce7dd4ba772a
Binary files /dev/null and b/doc/user/group/subgroups/img/mention_subgroups.png differ
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..ce5da07c61aca3e37b82a6a9aa18743d80c6efa6
--- /dev/null
+++ b/doc/user/group/subgroups/index.md
@@ -0,0 +1,164 @@
+# Subgroups
+
+> [Introduced][ce-2772] in GitLab 9.0.
+
+With subgroups (aka nested groups or hierarchical groups) you can have
+up to 20 levels of nested groups, which among other things can help you to:
+
+- **Separate internal / external organizations.** Since every group
+  can have its own visibility level, you are able to host groups for different
+  purposes under the same umbrella.
+- **Organize large projects.** For large projects, subgroups makes it
+  potentially easier to separate permissions on parts of the source code.
+- **Make it easier to manage people and control visibility.** Give people
+  different [permissions][] depending on their group [membership](#membership).
+
+## Overview
+
+A group can have many subgroups inside it, and at the same time a group can have
+only 1 parent group. It resembles a directory behavior or a nested items list:
+
+- Group 1
+  - Group 1.1
+  - Group 1.2
+      - Group 1.2.1
+      - Group 1.2.2
+          - Group 1.2.2.1
+
+In a real world example, imagine maintaining a GNU/Linux distribution with the
+first group being the name of the distro and subsequent groups split like:
+
+- Organization Group - GNU/Linux distro
+  - Category Subgroup - Packages
+      - (project) Package01
+      - (project) Package02
+  - Category Subgroup - Software
+      - (project) Core
+      - (project) CLI
+      - (project) Android app
+      - (project) iOS app
+  - Category Subgroup - Infra tools
+      - (project) Ansible playbooks
+
+Another example of GitLab as a company would be the following:
+
+- Organization Group - GitLab
+  - Category Subroup - Marketing
+      - (project) Design
+      - (project) General
+  - Category Subgroup - Software
+      - (project) GitLab CE
+      - (project) GitLab EE
+      - (project) Omnibus GitLab
+      - (project) GitLab Runner
+      - (project) GitLab Pages daemon
+  - Category Subgroup - Infra tools
+      - (project) Chef cookbooks
+  - Category Subgroup - Executive team
+
+---
+
+The maximum nested groups a group can have, including the first one in the
+hierarchy, is 21.
+
+Things like transferring or importing a project inside nested groups, work like
+when performing these actions the traditional way with the `group/project`
+structure.
+
+## Creating a subgroup
+
+>**Notes:**
+- You need to be an Owner of a group in order to be able to create
+  a subgroup. For more information check the [permissions table][permissions].
+- For a list of words that are not allowed to be used as group names see the
+  [`namespace_validator.rb` file][reserved] under the `RESERVED` and
+  `WILDCARD_ROUTES` lists.
+
+To create a subgroup:
+
+1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**.
+
+    ![Subgroups page](img/create_subgroup_button.png)
+
+1. Create a new group like you would normally do. Notice that the parent group
+   namespace is fixed under **Group path**. The visibility level can differ from
+   the parent group.
+
+    ![Subgroups page](img/create_new_group.png)
+
+1. Click the **Create group** button and you will be taken to the new group's
+   dashboard page.
+
+---
+
+You can follow the same process to create any subsequent groups.
+
+## Membership
+
+When you add a member to a subgroup, they inherit the membership and permission
+level from the parent group. This model allows access to nested groups if you
+have membership in one of its parents.
+
+The group permissions for a member can be changed only by Owners and only on
+the **Members** page of the group the member was added.
+
+You can tell if a member has inherited the permissions from a parent group by
+looking at the group's **Members** page.
+
+![Group members page](img/group_members.png)
+
+From the image above, we can deduct the following things:
+
+- There are 5 members that have access to the group `four`
+- User0 is a Reporter and has inherited their permissions from group `one`
+  which is above the hierarchy of group `four`
+- User1 is a Developer and has inherited their permissions from group
+  `one/two` which is above the hierarchy of group `four`
+- User2 is a Developer and has inherited their permissions from group
+  `one/two/three` which is above the hierarchy of group `four`
+- For User3 there is no indication of a parent group, therefore they belong to
+  group `four`, the one we're inspecting
+- Administrator is the Owner and member of **all** subgroups and for that reason,
+  same as User3, there is no indication of an ancestor group
+
+### Overriding the ancestor group membership
+
+>**Note:**
+You need to be an Owner of a group in order to be able to add members to it.
+
+To override a user's membership of an ancestor group (the first group they were
+added to), simply add the user in the new subgroup again, but with different
+permissions.
+
+For example, if User0 was first added to group `group-1/group-1-1` with Developer
+permissions, then they will inherit those permissions in every other subgroup
+of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`,
+you would add them again in that group as Master. Removing them from that group,
+the permissions will fallback to those of the ancestor group.
+
+## Mentioning subgroups
+
+Mentioning groups (`@group`) in issues, commits and merge requests, would
+notify all members of that group. Now with subgroups, there is a more granular
+support if you want to split your group's structure. Mentioning works as before
+and you can choose the group of people to be notified.
+
+![Mentioning subgroups](img/mention_subgroups.png)
+
+## Limitations
+
+Here's a list of what you can't do with subgroups:
+
+- [GitLab Pages](../../project/pages/index.md) are not currently working for
+  projects hosted under a subgroup. That means that only projects hosted under
+  the first parent group will work.
+- Group level labels don't work in subgroups / sub projects
+- It is not possible to share a project with a group that's an ancestor of
+  the group the project is in. That means you can only share as you walk down
+  the hierarchy. For example, `group/subgroup01/project` **cannot** be shared
+  with `group`, but can be shared with `group/subgroup02` or
+  `group/subgroup01/subgroup03`.
+
+[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
+[permissions]: ../../permissions.md#group
+[reserved]:  https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/user/img/award_emoji_comment_awarded.png
similarity index 100%
rename from doc/workflow/img/award_emoji_comment_awarded.png
rename to doc/user/img/award_emoji_comment_awarded.png
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/user/img/award_emoji_comment_picker.png
similarity index 100%
rename from doc/workflow/img/award_emoji_comment_picker.png
rename to doc/user/img/award_emoji_comment_picker.png
diff --git a/doc/user/img/award_emoji_select.png b/doc/user/img/award_emoji_select.png
new file mode 100644
index 0000000000000000000000000000000000000000..496acb29eecd9a9c584fea740bd11318c8013388
Binary files /dev/null and b/doc/user/img/award_emoji_select.png differ
diff --git a/doc/user/img/award_emoji_votes_sort_options.png b/doc/user/img/award_emoji_votes_sort_options.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd84b7f4f64744c1f4bde40cc57f1bd2cfc90982
Binary files /dev/null and b/doc/user/img/award_emoji_votes_sort_options.png differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c14db17b0e6329f96f455ace500aa5f044b1967f..97de428d11d7fbc617b69d02ea4c42783371c52e 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_.
 
 Strong emphasis, aka bold, with **asterisks** or __underscores__.
 
-Combined emphasis with **asterisks and _underscores_**.
+Combined emphasis with **_asterisks and underscores_**.
 
 Strikethrough uses two tildes. ~~Scratch this.~~
 ```
@@ -576,7 +576,7 @@ Quote break.
 
 You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
 
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes.  In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes.  In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements.
 
 ```no-highlight
 <dl>
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b49a244160a837839bae71e5df8fc7792bee8065..0ea6d01411f386020d32a1d250cba10afee6e6b4 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -81,6 +81,7 @@ group.
 |-------------------------|-------|----------|-----------|--------|-------|
 | Browse group            | ✓     | ✓        | ✓         | ✓      | ✓     |
 | Edit group              |       |          |           |        | ✓     |
+| Create subgroup         |       |          |           |        | ✓     |
 | Create project in group |       |          |           | ✓      | ✓     |
 | Manage group members    |       |          |           |        | ✓     |
 | Remove group            |       |          |           |        | ✓     |
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index a23ad79ae1df43b5e0efd7465959321c176a5f63..63a3d3c472ec8d315c4f2a8615229276317a4976 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -213,5 +213,16 @@ your GitLab server's time is synchronized via a service like NTP.  Otherwise,
 you may have cases where authorization always fails because of time differences.
 
 [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
-[FreeOTP]: https://fedorahosted.org/freeotp/
+[FreeOTP]: https://freeotp.github.io/
 [YubiKey]: https://www.yubico.com/products/yubikey-hardware/
+
+- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from
+multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at 
+the time of registration, and cannot be used for other hostnames/FQDNs.
+
+    For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
+
+    - The user logs in via `first.host.xyz` and registers their U2F key.
+    - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds.
+    - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because 
+    the U2F key has only been registered on `first.host.xyz`.
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index c5b2266ff19a56db6d41af2703ee8b543612b11d..7524e70957f22d3cee8bee1acd55308d54ed3f4f 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -248,4 +248,4 @@ Once the right permissions were set, the error will go away.
 
 [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
 [docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry
+[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
deleted file mode 100644
index f769dece242e5260425eb805e1285ae9e44b3059..0000000000000000000000000000000000000000
--- a/doc/user/project/integrations/builds_emails.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Enabling build emails
-
-By enabling this service, you will be able to receive e-mail notifications about
-the result status of your builds.
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Builds emails** service to configure it.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317ff47596210461924bdd635faeaab1..f52acf4ef3b2a688e52ceeb730dc2a29babf7bf8 100644
Binary files a/doc/user/project/integrations/img/mattermost_configuration.png and b/doc/user/project/integrations/img/mattermost_configuration.png differ
diff --git a/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f5a44f882093f02059a861cabfc24d00f8e29df
Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png
new file mode 100644
index 0000000000000000000000000000000000000000..e30cba211e6b58734ab780a13e4b3e4a84dfbd12
Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_node_name.png b/doc/user/project/integrations/img/prometheus_gcp_node_name.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea28943145452d396bc22dfaa052a05182413109
Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_gcp_node_name.png differ
diff --git a/doc/user/project/integrations/img/prometheus_service_configuration.png b/doc/user/project/integrations/img/prometheus_service_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7dfe8748173e46967fd4912d4951c328f58c0d3
Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_service_configuration.png differ
diff --git a/doc/user/project/integrations/img/prometheus_yaml_deploy.png b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
new file mode 100644
index 0000000000000000000000000000000000000000..978cd7eaa50c466562e2b9c51be7d139c543096f
Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_yaml_deploy.png differ
diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b535856828b8a1d36086cb81f3c639..527824fc3eb0e4cf6d81676b486dfda332c9c1c7 100644
Binary files a/doc/user/project/integrations/img/slack_configuration.png and b/doc/user/project/integrations/img/slack_configuration.png differ
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index cc67e667472c8be3e77dc82c66784fc5c421cac1..2a890acde4dcb90cf428ff47ae63c8b61932659e 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -49,7 +49,8 @@ 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
+- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data.
+- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data.
 
 ## Web terminals
 
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 09ba9994d3ad537d75cb09207657b151ec73c754..3e77823a6aa6c9cfe5808c8a0f3a82ba891f1a95 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -24,23 +24,22 @@ There, you will see a checkbox with the following events that can be triggered:
 
 - Push
 - Issue
+- Confidential issue
 - Merge request
 - Note
 - Tag push
-- Build
+- Pipeline
 - 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.
+Below each of these event checkboxes, you have an input field to enter
+which Mattermost channel you want to send that event message. Enter your preferred channel handle (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... |
+| **Webhook**  | The incoming webhook URL 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. |
-
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
 
 ![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md
new file mode 100644
index 0000000000000000000000000000000000000000..6aefe5dbded69c391c0669269d37f3cbd9f4e995
--- /dev/null
+++ b/doc/user/project/integrations/mock_ci.md
@@ -0,0 +1,13 @@
+# Mock CI Service
+
+**NB: This service is only listed if you are in a development environment!**
+
+To setup the mock CI service server, respond to the following endpoints
+
+- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json`
+   - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }`
+   - If the service returns a 404, it is interpreted as `pending`
+- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}`
+   - Just where the build is linked to, doesn't matter if implemented
+
+For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index a3a163a4c6ba986bec1d481160ffe1f4654869c7..25400633de5894a5ccd2b0d850b06ce55f1a1e89 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -32,7 +32,6 @@ Click on the service links to see further configuration instructions and details
 | Assembla 	| Project Management Software (Source Commits Endpoint) |
 | [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
 | Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) |	Email the builds status to a list of recipients |
 | [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
 | Campfire | Simple web-based real-time group chat |
 | Custom Issue Tracker | Custom issue tracker |
@@ -48,9 +47,11 @@ Click on the service links to see further configuration instructions and details
 | [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 |
+| Pipelines emails | Email the pipeline status to a list of recipients |
 | [Slack Notifications](slack.md) | Receive event notifications in Slack |
 | [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
 | PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
 | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
 | [Redmine](redmine.md) | Redmine issue tracker |
 
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
new file mode 100644
index 0000000000000000000000000000000000000000..676a21e85c4ce688474eb93900c3d8b40e4bd32a
--- /dev/null
+++ b/doc/user/project/integrations/prometheus.md
@@ -0,0 +1,196 @@
+# Prometheus integration
+
+> [Introduced][ce-8935] in GitLab 9.0.
+
+GitLab offers powerful integration with [Prometheus] for monitoring your apps.
+Metrics are retrieved from the configured Prometheus server, and then displayed
+within the GitLab interface.
+
+Each project can be configured with its own specific Prometheus server, see the
+[configuration](#configuration) section for more details. If you have a single
+Prometheus server which monitors all of your infrastructure, you can pre-fill
+the settings page with a default template. To configure the template, see the
+[Services templates](services_templates.md) document.
+
+## Requirements
+
+Integration with Prometheus requires the following:
+
+1. GitLab 9.0 or higher
+1. Your app must be deployed on [Kubernetes][]
+1. Prometheus must be configured to collect Kubernetes metrics
+1. Each metric must be have a label to indicate the environment
+1. GitLab must have network connectivity to the Prometheus sever
+
+There are a few steps necessary to set up integration between Prometheus and
+GitLab.
+
+## Configuring Prometheus to collect Kubernetes metrics
+
+In order for Prometheus to collect Kubernetes metrics, you first must have a
+Prometheus server up and running. You have two options here:
+
+- If you installed Omnibus GitLab inside of Kubernetes, you can simply use the
+  [bundled version of Prometheus][promgldocs]. In that case, follow the info in the
+  [Omnibus GitLab section](#configuring-omnibus-gitlab-prometheus-to-monitor-kubernetes)
+  below.
+- If you are using GitLab.com or installed GitLab outside of Kubernetes, you
+  will likely need to run a Prometheus server within the Kubernetes cluster.
+  Once installed, the easiest way to monitor Kubernetes is to simply use
+  Prometheus' support for [Kubernetes Service Discovery][prometheus-k8s-sd].
+  In that case, follow the instructions on
+  [configuring your own Prometheus server within Kubernetes](#configuring-your-own-prometheus-server-within-kubernetes).
+
+### Configuring Omnibus GitLab Prometheus to monitor Kubernetes
+
+With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
+version of Prometheus to collect the required metrics.
+
+1. Read how to configure the bundled Prometheus server in the
+   [Administration guide][gitlab-prometheus-k8s-monitor].
+1. Now that Prometheus is configured, proceed on
+   [configuring the Prometheus project service in GitLab](#configuration-in-gitlab).
+
+### Configuring your own Prometheus server within Kubernetes
+
+Setting up and configuring Prometheus within Kubernetes is quick and painless.
+The Prometheus project provides an [official Docker image][prometheus-docker-image]
+which we can use as a starting point.
+
+To get started quickly, we have provided a [sample YML file][prometheus-yml]
+that can be used as a template. This file will create a `prometheus` **Namespace**,
+**Service**, **Deployment**, and **ConfigMap** in Kubernetes. You can upload
+this file to the Kubernetes dashboard using **+ Create** at the top right.
+
+![Deploy Prometheus](img/prometheus_yaml_deploy.png)
+
+Or use `kubectl`:
+
+```bash
+kubectl apply -f path/to/prometheus.yml
+```
+
+Once deployed, you should see the Prometheus service, deployment, and
+pod start within the `prometheus` namespace. The server will begin to collect
+metrics from each Kubernetes Node in the cluster, based on the configuration
+provided in the template.
+
+Since GitLab is not running within Kubernetes, the template provides external
+network access via a `NodePort` running on `30090`. This method allows access
+to be controlled using provider firewall rules, like within Google Compute Engine.
+
+Since a `NodePort` does not automatically have firewall rules created for it,
+one will need to be created manually to allow access. In GCP/GKE, you will want
+to confirm the Node that the Prometheus pod is running on. This can be done
+either by looking at the Pod in the Kubernetes dashboard, or by running:
+
+```bash
+kubectl describe pods -n prometheus
+```
+
+Next on GKE, we need to get the `tag` of the Node or VM Instance, so we can
+create an accurate firewall rule. The easiest way to do this is to go into the
+Google Cloud Platform Compute console and select the VM instance that matches
+the name of the Node gathered from the step above. In this case, the node tag
+needed is `gke-prometheus-demo-5d5ada10-node`. Also make a note of the
+**External IP**, which will be the IP address the Prometheus server is reachable
+on.
+
+![GCP Node Detail](img/prometheus_gcp_node_name.png)
+
+Armed with the proper Node tag, the firewall rule can now be created
+specifically for this node. To create the firewall rule, open the Google Cloud
+Platform Networking console, and select **Firewall Rules**.
+
+Create a new rule:
+
+- Specify the source IP range to match your desired access list, which should
+  include your GitLab server. A sample of GitLab.com's IP address range is
+  available [in this issue][gitlab.com-ip-range], but note that GitLab.com's IPs
+  are subject to change without prior notification.
+- Allowed protocol and port should be `tcp:30090`.
+- The target tags should match the Node tag identified earlier in this step.
+
+![GCP Firewall Rule](img/prometheus_gcp_firewall_rule.png)
+
+---
+
+Now that Prometheus is configured, proceed to
+[configure the Prometheus project service in GitLab](##configuration-in-gitlab).
+
+## Configuration in GitLab
+
+The actual configuration of Prometheus integration within GitLab is very simple.
+All you will need is the DNS or IP address of the Prometheus server you'd like
+to integrate with.
+
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click the **Prometheus** service
+1. Provide the base URL of the your server, for example `http://prometheus.example.com/`.
+   The **Test Settings** button can be used to confirm connectivity from GitLab
+   to the Prometheus server.
+
+![Configure Prometheus Service](img/prometheus_service_configuration.png)
+
+## Metrics and Labels
+
+GitLab retrieves performance data from two metrics, `container_cpu_usage_seconds_total`
+and `container_memory_usage_bytes`. These metrics are collected from the
+Kubernetes pods via Prometheus, and report CPU and Memory utilization of each
+container or Pod running in the cluster.
+
+In order to isolate and only display relevant metrics for a given environment
+however, GitLab needs a method to detect which pods are associated. To do that,
+GitLab will specifically request metrics that have an `environment` tag that
+matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
+
+If you are using [GitLab Auto-Deploy][autodeploy] and one of the methods of
+configuring Prometheus above, the `environment` will be automatically added.
+
+### GitLab Prometheus queries
+
+The queries utilized by GitLab are shown in the following table.
+
+| Metric | Query |
+| ------ | ----- |
+| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` |
+| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
+
+## Monitoring CI/CD Environments
+
+Once configured, GitLab will attempt to retrieve performance metrics for any
+environment which has had a successful deployment. If monitoring data was
+successfully retrieved, a metrics button will appear on the environment's
+detail page.
+
+![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
+
+Clicking on the metrics button will display a new page, showing up to the last
+8 hours of performance data. It may take a minute or two for data to appear
+after initial deployment.
+
+## Troubleshooting
+
+If the metrics button is not appearing, then one of a few issues may be
+occurring:
+
+- GitLab is not able to reach the Prometheus server. A test request can be sent
+  to the Prometheus server from the [Prometheus Service](#configuration-in-gitlab)
+  configuration screen.
+- No successful deployments have occurred to this environment.
+- Prometheus does not have performance data for this environment, or the metrics
+  are not labeled correctly. To test this, connect to the Prometheus server and
+  [run a query](#gitlab-prometheus-queries), replacing `$CI_ENVIRONMENT_SLUG`
+  with the name of your environment.
+
+[autodeploy]: ../../../ci/autodeploy/index.md
+[kubernetes]: https://kubernetes.io
+[prometheus-k8s-sd]: https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>
+[prometheus]: https://prometheus.io
+[gitlab-prometheus-k8s-monitor]: ../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes
+[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
+[prometheus-yml]:samples/prometheus.yml
+[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
+[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
+[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
+[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/integrations/samples/prometheus.yml b/doc/user/project/integrations/samples/prometheus.yml
new file mode 100644
index 0000000000000000000000000000000000000000..01bbcaffe1e729248f0d6a9ce5f1b88bd39dfbd0
--- /dev/null
+++ b/doc/user/project/integrations/samples/prometheus.yml
@@ -0,0 +1,69 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: prometheus
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: prometheus
+  namespace: prometheus
+data:
+  prometheus.yml: |-
+      scrape_configs:
+      - job_name: 'kubernetes-nodes'
+        scheme: https
+        tls_config:
+          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+          insecure_skip_verify: true
+        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+        kubernetes_sd_configs:
+        - role: node
+        metric_relabel_configs:
+        - source_labels: [pod_name]
+          target_label: environment
+          regex: (.+)-.+-.+
+          replacement: $1
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: prometheus
+  namespace: prometheus
+spec:
+  selector:
+    app: prometheus
+  ports:
+  - name: prometheus
+    protocol: TCP
+    port: 9090
+    nodePort: 30090
+  type: NodePort
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: prometheus
+  namespace: prometheus
+spec:
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: prometheus
+    spec:
+      containers:
+      - name: prometheus
+        image: prom/prometheus:latest
+        args:
+          - '-config.file=/prometheus-data/prometheus.yml'
+        ports:
+        - name: prometheus
+          containerPort: 9090
+        volumeMounts:
+        - name: data-volume
+          mountPath: /prometheus-data
+      volumes:
+      - name: data-volume
+        configMap:
+          name: prometheus
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index 57a9492044b64c3301d5c6462173128e93780992..e8b238351ca25352a848ad253d33e04e2c5adaf0 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -21,23 +21,23 @@ There, you will see a checkbox with the following events that can be triggered:
 
 - Push
 - Issue
+- Confidential issue
 - Merge request
 - Note
 - Tag push
-- Build
+- Pipeline
 - Wiki page
 
-Bellow each of these event checkboxes, you will have an input field to insert
-which Slack channel you want to send that event message, with `#general`
-being the default. Enter your preferred channel **without** the hash sign (`#`).
+Below each of these event checkboxes, you have an input field to enter
+which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
 
 At the end, fill in your Slack details:
 
 | Field | Description |
 | ----- | ----------- |
 | **Webhook**  | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
-| **Username** | Optional username which can be on messages sent to slack. 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. |
+| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
 
 After you are all done, click **Save changes** for the changes to take effect.
 
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ed1e867f5fb52ac7767ef8a6c6a47a679992e3a2..dbdc93a77a8eb93a9a2ed727bd0abfafd0d14d30 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -250,7 +250,19 @@ X-Gitlab-Event: Issue Hook
     "name": "User1",
     "username": "user1",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
-  }
+  },
+  "labels": [{
+    "id": 206,
+    "title": "API",
+    "color": "#ffffff",
+    "project_id": 14,
+    "created_at": "2013-12-03T17:15:43Z",
+    "updated_at": "2013-12-03T17:15:43Z",
+    "template": false,
+    "description": "API related issues",
+    "type": "ProjectLabel",
+    "group_id": 41
+  }]
 }
 ```
 ### Comment events
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 3199d370a582a3cdb414a7af40864524aa061a01..5aa8337b75db33f115046309b8f844fa49bf03f6 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -28,7 +28,7 @@ Below is a table of the definitions used for GitLab's Issue Board.
 | --------------  | ------------- |
 | **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
 | **List**        | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
-| **Card**        | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
+| **Card**        | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. |
 
 There are two types of lists, the ones you create based on your labels, and
 one default:
@@ -45,6 +45,7 @@ In short, here's a list of actions you can take in an Issue Board:
 - [Create a new list](#creating-a-new-list).
 - [Delete an existing list](#deleting-a-list).
 - Drag issues between lists.
+- Re-order issues in lists.
 - Drag and reorder the lists themselves.
 - Change issue labels on-the-fly while dragging issues between lists.
 - Close an issue if you drag it to the **Done** list.
@@ -114,6 +115,13 @@ board itself.
 
 ![Remove issue from list](img/issue_boards_remove_issue.png)
 
+## Re-ordering an issue in a list
+
+> Introduced in GitLab 9.0.
+
+Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
+an issue into the order you want.
+
 ## Filtering issues
 
 You should be able to use the filters on top of your Issue Board to show only
@@ -176,7 +184,6 @@ A few things to remember:
 - Clicking on the issue title inside a card will take you to that issue.
 - Clicking on a label inside a card will quickly filter the entire Issue Board
   and show only the issues from all lists that have that label.
-- Issues inside lists are [ordered by priority][label-priority].
 - For performance and visibility reasons, each list shows the first 20 issues
   by default. If you have more than 20 issues start scrolling down and the next
   20 will appear.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index cf1d9cbe69cc7cd429081802d4edc9411844cf55..8ec7adad172c97032c6da7b795e4535ab7362d62 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -65,7 +65,7 @@ issues and merge requests assigned to each label.
 >   https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
 
 Prioritized labels are like any other label, but sorted by priority. This allows
-you to sort issues and merge requests by priority.
+you to sort issues and merge requests by label priority.
 
 To prioritize labels, navigate to your project's **Issues > Labels** and click
 on the star icon next to them to put them in the priority list. Click on the
@@ -77,9 +77,13 @@ having their priority set to null.
 
 ![Prioritize labels](img/labels_prioritize.png)
 
-Now that you have labels prioritized, you can use the 'Priority' filter in the
-issues or merge requests tracker. Those with the highest priority label, will
-appear on top.
+Now that you have labels prioritized, you can use the 'Priority' and 'Label
+priority' filters in the issues or merge requests tracker.
+
+The 'Label priority' filter puts issues with the highest priority label on top.
+
+The 'Priority' filter sorts issues by their soonest milestone due date, then by
+label priority.
 
 ![Filter labels by priority](img/labels_filter_by_priority.png)
 
@@ -156,4 +160,3 @@ mouse over the label in the issue tracker or wherever else the label is
 rendered.
 
 ![Label tooltips](img/labels_description_tooltip.png)
-
diff --git a/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
new file mode 100644
index 0000000000000000000000000000000000000000..b15447ec2909687ae11747fb880419c6cec96bb5
Binary files /dev/null and b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
deleted file mode 100644
index f50a1be24f221d92684b23aa0a42c77966cc42eb..0000000000000000000000000000000000000000
Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
deleted file mode 100644
index ddc58ff26302b073bd14d1f32e8f7e56fa7921ff..0000000000000000000000000000000000000000
Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
deleted file mode 100644
index a98636ee35905bc2fb488d6d26d87ae80765d4fb..0000000000000000000000000000000000000000
Binary files a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
new file mode 100644
index 0000000000000000000000000000000000000000..33f5a4a7a02620695006d6e1040f6a666588ecdd
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png
similarity index 100%
rename from doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
rename to doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..9629ed99838b80594dc595c0071564474090cc83
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0691437c658c9f152897108184d059e7eac7393
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png differ
diff --git a/doc/user/project/merge_requests/img/new_issue_for_discussion.png b/doc/user/project/merge_requests/img/new_issue_for_discussion.png
new file mode 100644
index 0000000000000000000000000000000000000000..93c9dad89216b2575d03bf99553a2ca398743025
Binary files /dev/null and b/doc/user/project/merge_requests/img/new_issue_for_discussion.png differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussion.png b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ee0653b2bae3779f48d70b53382ce1a4f336525
Binary files /dev/null and b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
index 9fdd387676c095fdb3383378a74ec92ee0b337b8..3fe0a66667870d749f8dde4a5c77462dbfc00ad5 100644
Binary files a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png and b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
index 8c7ce215ae0a93029be9784eef842ae7f49e479b..e0ee6a39ffdd0234a6571d9ba98a2ea9e6665788 100644
Binary files a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png and b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png differ
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index d4b85676d190ffde20e559f67c05b46f12e5a8a0..230e957f0450388f9e223ddd40c796e3c05c5ac9 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -53,12 +53,18 @@ are resolved.
 
 ## Move all unresolved discussions in a merge request to an issue
 
-> [Introduced][ce-7180] in GitLab 8.15.
+> [Introduced][ce-8266]
 
-To delegate unresolved discussions to a new issue you can click the link **open
-an issue to resolve them later**.
+To continue all open discussions in a merge request, click the button **Resolve
+all discussions in new issue**
 
-![Open new issue from unresolved discussions](img/resolve_discussion_open_issue.png)
+![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
+
+Alternatively, when your project only accepts merge requests when all discussions
+are resolved, there will be an **open an issue to resolve them later** link in
+the merge request-widget.
+
+![Link in merge request widget](img/resolve_discussion_open_issue.png)
 
 This will prepare an issue with content referring to the merge request and
 discussions.
@@ -72,9 +78,28 @@ add a note referring to the newly created issue.
 
 You can now proceed to merge the merge request from the UI.
 
+## Moving a single discussion to a new issue
+
+> [Introduced][ce-8266]
+
+To create a new issue for a single discussion, you can use the **Resolve this
+discussion in a new issue** button.
+
+![Create issue for discussion](img/new_issue_for_discussion.png)
+
+This will direct you to a new issue prefilled with the content of the
+discussion, similar to the issues created for delegating multiple
+discussions at once.
+
+![New issue for a single discussion](img/preview_issue_for_discussion.png)
+
+Saving the issue will mark the discussion as resolved and add a note
+to the discussion referencing the new issue.
+
 [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
 [ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
 [ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
+[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
 [resolve-discussion-button]: img/resolve_discussion_button.png
 [resolve-comment-button]: img/resolve_comment_button.png
 [discussion-view]: img/discussion_view.png
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index c63a408505f041e3ce1c187f761f7cc9f29901dc..bdd7d0022e69257a62604bf26983c31e5aab8807 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -5,7 +5,7 @@ more CI jobs running, you can set it to be merged automatically when the
 jobs pipeline succeeds. This way, you don't have to wait for the jobs to
 finish and remember to merge the request manually.
 
-![Enable](img/merge_when_build_succeeds_enable.png)
+![Enable](img/merge_when_pipeline_succeeds_enable.png)
 
 When you hit the "Merge When Pipeline Succeeds" button, the status of the merge
 request will be updated to represent the impending merge. If you cannot wait
@@ -16,7 +16,7 @@ Both team developers and the author of the merge request have the option to
 cancel the automatic merge if they find a reason why it shouldn't be merged
 after all.
 
-![Status](img/merge_when_build_succeeds_status.png)
+![Status](img/merge_when_pipeline_succeeds_status.png)
 
 When the pipeline succeeds, the merge request will automatically be merged.
 When the pipeline fails, the author gets a chance to retry any failed jobs,
@@ -32,15 +32,16 @@ changes to be reviewed.
 > **Note:**
 You need to have jobs configured to enable this feature.
 
-You can prevent merge requests from being merged if their pipeline did not succeed.
+You can prevent merge requests from being merged if their pipeline did not succeed
+or if there are discussions to be resolved.
 
 Navigate to your project's settings page, select the
 **Only allow merge requests to be merged if the pipeline succeeds** check box and
 hit **Save** for the changes to take effect.
 
-![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
+![Only allow merge if pipeline succeeds settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
 
 From now on, every time the pipeline fails you will not be able to merge the
 merge request from the UI, until you make all relevant jobs pass.
 
-![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
+![Only allow merge if pipeline succeeds message](img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 5f631f630500ef16b8641882cab07dd021d95e03..b559d132590b3ee5c105c8b5c0ac444fb1d052ff 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -119,7 +119,7 @@ And then the users could also use it in their CI jobs all Docker related
 commands to interact with GitLab Container Registry. For example:
 
 ```
-docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
 ```
 
 Using single token had multiple security implications:
@@ -208,7 +208,7 @@ This is how an example usage can look like:
 ```
 test:
   script:
-    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
     - docker pull $CI_REGISTRY/group/other-project:latest
     - docker run $CI_REGISTRY/group/other-project:latest
 ```
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
new file mode 100644
index 0000000000000000000000000000000000000000..35af48724f22a2eb9f6e5544c8d8c7e8a43e3f90
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -0,0 +1,385 @@
+# GitLab Pages from A to Z: Part 4
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- **Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages**
+
+## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages
+
+[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves
+numerous purposes, to build, test, and deploy your app
+from GitLab through
+[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+methods. You will need it to build your website with GitLab Pages,
+and deploy it to the Pages server.
+
+What this file actually does is telling the
+[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts
+as you would do from the command line. The Runner acts as your
+terminal. GitLab CI tells the Runner which commands to run.
+Both are built-in in GitLab, and you don't need to set up
+anything for them to work.
+
+Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+and GitLab Runner is out of the scope of this guide, but we'll
+need to understand just a few things to be able to write our own
+`.gitlab-ci.yml` or tweak an existing one. It's an
+[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file,
+with its own syntax. You can always check your CI syntax with
+the [GitLab CI Lint Tool](https://gitlab.com/ci/lint).
+
+**Practical Example:**
+
+Let's consider you have a [Jekyll](https://jekyllrb.com/) site.
+To build it locally, you would open your terminal, and run `jekyll build`.
+Of course, before building it, you had to install Jekyll in your computer.
+For that, you had to open your terminal and run `gem install jekyll`.
+Right? GitLab CI + GitLab Runner do the same thing. But you need to
+write in the `.gitlab-ci.yml` the script you want to run so
+GitLab Runner will do it for you. It looks more complicated then it
+is. What you need to tell the Runner:
+
+```
+$ gem install jekyll
+$ jekyll build
+```
+
+### Script
+
+To transpose this script to Yaml, it would be like this:
+
+```yaml
+script:
+  - gem install jekyll
+  - jekyll build
+```
+
+### Job
+
+So far so good. Now, each `script`, in GitLab is organized by
+a `job`, which is a bunch of scripts and settings you want to
+apply to that specific task.
+
+```yaml
+job:
+  script:
+  - gem install jekyll
+  - jekyll build
+```
+
+For GitLab Pages, this `job` has a specific name, called `pages`,
+which tells the Runner you want that task to deploy your website
+with GitLab Pages:
+
+```yaml
+pages:
+  script:
+  - gem install jekyll
+  - jekyll build
+```
+
+### The `public` directory
+
+We also need to tell Jekyll where do you want the website to build,
+and GitLab Pages will only consider files in a directory called `public`.
+To do that with Jekyll, we need to add a flag specifying the
+[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the
+built website: `jekyll build -d public`. Of course, we need
+to tell this to our Runner:
+
+```yaml
+pages:
+  script:
+  - gem install jekyll
+  - jekyll build -d public
+```
+
+### Artifacts
+
+We also need to tell the Runner that this _job_ generates
+_artifacts_, which is the site built by Jekyll.
+Where are these artifacts stored? In the `public` directory:
+
+```yaml
+pages:
+  script:
+  - gem install jekyll
+  - jekyll build -d public
+  artifacts:
+    paths:
+    - public
+```
+
+The script above would be enough to build your Jekyll
+site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
+template originated by `jekyll new project` requires
+[Bundler](http://bundler.io/) to install Jekyll dependencies
+and the default theme. To adjust our script to meet these new
+requirements, we only need to install and build Jekyll with Bundler:
+
+```yaml
+pages:
+  script:
+  - bundle install
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+```
+
+That's it! A `.gitlab-ci.yml` with the content above would deploy
+your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
+configuration for our example. On the steps below, we'll refine
+the script by adding extra options to our GitLab CI.
+
+Artifacts will be automatically deleted once GitLab Pages got deployed.
+You can preserve artifacts for limited time by specifying the expiry time.
+
+### Image
+
+At this point, you probably ask yourself: "okay, but to install Jekyll
+I need Ruby. Where is Ruby on that script?". The answer is simple: the
+first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a
+[Docker](https://www.docker.com/) image specifying what do you need in
+your container to run that script:
+
+```yaml
+image: ruby:2.3
+
+pages:
+  script:
+  - bundle install
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+```
+
+In this case, you're telling the Runner to pull this image, which
+contains Ruby 2.3 as part of its file system. When you don't specify
+this image in your configuration, the Runner will use a default
+image, which is Ruby 2.1.
+
+If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll
+need to specify which image you want to use, and this image should
+contain NodeJS as part of its file system. E.g., for a
+[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`.
+
+>**Note:**
+We're not trying to explain what a Docker image is,
+we just need to introduce the concept with a minimum viable
+explanation. To know more about Docker images, please visit
+their website or take a look at a
+[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here.
+
+Let's go a little further.
+
+### Branching
+
+If you use GitLab as a version control platform, you will have your
+branching strategy to work on your project. Meaning, you will have
+other branches in your project, but you'll want only pushes to the
+default branch (usually `master`) to be deployed to your website.
+To do that, we need to add another line to our CI, telling the Runner
+to only perform that _job_ called `pages` on the `master` branch `only`:
+
+```yaml
+image: ruby:2.3
+
+pages:
+  script:
+  - bundle install
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+```
+
+### Stages
+
+Another interesting concept to keep in mind are build stages.
+Your web app can pass through a lot of tests and other tasks
+until it's deployed to staging or production environments.
+There are three default stages on GitLab CI: build, test,
+and deploy. To specify which stage your _job_ is running,
+simply add another line to your CI:
+
+```yaml
+image: ruby:2.3
+
+pages:
+  stage: deploy
+  script:
+  - bundle install
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+```
+
+You might ask yourself: "why should I bother with stages
+at all?" Well, let's say you want to be able to test your
+script and check the built site before deploying your site
+to production. You want to run the test exactly as your
+script will do when you push to `master`. It's simple,
+let's add another task (_job_) to our CI, telling it to
+test every push to other branches, `except` the `master` branch:
+
+```yaml
+image: ruby:2.3
+
+pages:
+  stage: deploy
+  script:
+  - bundle install
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+
+test:
+  stage: test
+  script:
+  - bundle install
+  - bundle exec jekyll build -d test
+  artifacts:
+    paths:
+    - test
+  except:
+  - master
+```
+
+The `test` job is running on the stage `test`, Jekyll
+will build the site in a directory called `test`, and
+this job will affect all the branches except `master`.
+
+The best benefit of applying _stages_ to different
+_jobs_ is that every job in the same stage builds in
+parallel. So, if your web app needs more than one test
+before being deployed, you can run all your test at the
+same time, it's not necessary to wait one test to finish
+to run the other. Of course, this is just a brief
+introduction of GitLab CI and GitLab Runner, which are
+tools much more powerful than that. This is what you
+need to be able to create and tweak your builds for
+your GitLab Pages site.
+
+### Before Script
+
+To avoid running the same script multiple times across
+your _jobs_, you can add the parameter `before_script`,
+in which you specify which commands you want to run for
+every single _job_. In our example, notice that we run
+`bundle install` for both jobs, `pages` and `test`.
+We don't need to repeat it:
+
+```yaml
+image: ruby:2.3
+
+before_script:
+  - bundle install
+
+pages:
+  stage: deploy
+  script:
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+
+test:
+  stage: test
+  script:
+  - bundle exec jekyll build -d test
+  artifacts:
+    paths:
+    - test
+  except:
+  - master
+```
+
+### Caching Dependencies
+
+If you want to cache the installation files for your
+projects dependencies, for building faster, you can
+use the parameter `cache`. For this example, we'll
+cache Jekyll dependencies in a `vendor` directory
+when we run `bundle install`:
+
+```yaml
+image: ruby:2.3
+
+cache:
+  paths:
+  - vendor/
+
+before_script:
+  - bundle install --path vendor
+
+pages:
+  stage: deploy
+  script:
+  - bundle exec jekyll build -d public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+
+test:
+  stage: test
+  script:
+  - bundle exec jekyll build -d test
+  artifacts:
+    paths:
+    - test
+  except:
+  - master
+```
+
+For this specific case, we need to exclude `/vendor`
+from Jekyll `_config.yml` file, otherwise Jekyll will
+understand it as a regular directory to build
+together with the site:
+
+```yml
+exclude:
+  - vendor
+```
+
+There we go! Now our GitLab CI not only builds our website,
+but also **continuously test** pushes to feature-branches,
+**caches** dependencies installed with Bundler, and
+**continuously deploy** every push to the `master` branch.
+
+## Advanced GitLab CI for GitLab Pages
+
+What you can do with GitLab CI is pretty much up to your
+creativity. Once you get used to it, you start creating
+awesome scripts that automate most of tasks you'd do
+manually in the past. Read through the
+[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+to understand how to go even further on your scripts.
+
+- On this blog post, understand the concept of
+[using GitLab CI `environments` to deploy your
+web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/).
+- On this post, learn [how to run jobs sequentially,
+in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+- On this blog post, we go through the process of
+[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+to deploy this website you're looking at, docs.gitlab.com.
+- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+
+|||
+|:--|--:|
+|[**← Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates**](getting_started_part_three.md)||
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
new file mode 100644
index 0000000000000000000000000000000000000000..582a4afbab4adb748c5a2d43ebe84bc4b4a39cca
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -0,0 +1,106 @@
+# GitLab Pages from A to Z: Part 1
+
+- **Part 1: Static sites and GitLab Pages domains**
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## GitLab Pages form A to Z
+
+This is a comprehensive guide, made for those who want to
+publish a website with GitLab Pages but aren't familiar with
+the entire process involved.
+
+This [first part](#what-you-need-to-know-before-getting-started) of this series will present you to the concepts of
+static sites, and go over how the default Pages domains work.
+
+The [second part](getting_started_part_two.md) covers how to get started with GitLab Pages: deploy
+a website from a forked project or create a new one from scratch.
+
+The [third part](getting_started_part_three.md) will show you how to set up a custom domain or subdomain
+to your site already deployed.
+
+The [fourth part](getting_started_part_four.md) will show you how to create and tweak GitLab CI for
+GitLab Pages.
+
+To **enable** GitLab Pages for GitLab CE (Community Edition)
+and GitLab EE (Enterprise Edition), please read the
+[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html),
+and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s).
+
+>**Note:**
+For this guide, we assume you already have GitLab Pages
+server up and running for your GitLab instance.
+
+## What you need to know before getting started
+
+Before we begin, let's understand a few concepts first.
+
+### Static sites
+
+GitLab Pages only supports static websites, meaning,
+your output files must be HTML, CSS, and JavaScript only.
+
+To create your static site, you can either hardcode in HTML,
+CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/)
+to simplify your code and build the static site for you,
+which is highly recommendable and much faster than hardcoding.
+
+#### Further Reading
+
+- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
+- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- Fork an [example project](https://gitlab.com/pages) to build your website based upon
+
+### GitLab Pages domain
+
+If you set up a GitLab Pages project on GitLab.com,
+it will automatically be accessible under a
+[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/).
+The `namespace` is defined by your username on GitLab.com,
+or the group name you created this project under.
+
+>**Note:**
+If you use your own GitLab instance to deploy your
+site with GitLab Pages, check with your sysadmin what's your
+Pages wildcard domain. This guide is valid for any GitLab instance,
+you just need to replace Pages wildcard domain on GitLab.com
+(`*.gitlab.io`) with your own.
+
+#### Practical examples
+
+**Project Websites:**
+
+- You created a project called `blog` under your username `john`,
+therefore your project URL is `https://gitlab.com/john/blog/`.
+Once you enable GitLab Pages for this project, and build your site,
+it will be available under `https://john.gitlab.io/blog/`.
+- You created a group for all your websites called `websites`,
+and a project within this group is called `blog`. Your project
+URL is `https://gitlab.com/websites/blog/`. Once you enable
+GitLab Pages for this project, the site will live under
+`https://websites.gitlab.io/blog/`.
+
+**User and Group Websites:**
+
+- Under your username, `john`, you created a project called
+`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`.
+Once you enable GitLab Pages for your project, your website
+will be published under `https://john.gitlab.io`.
+- Under your group `websites`, you created a project called
+`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project,
+your website will be published under `https://websites.gitlab.io`.
+
+**General example:**
+
+- On GitLab.com, a project site will always be available under
+`https://namespace.gitlab.io/project-name`
+- On GitLab.com, a user or group website will be available under
+`https://namespace.gitlab.io/`
+- On your GitLab instance, replace `gitlab.io` above with your
+Pages server domain. Ask your sysadmin for this information.
+
+|||
+|:--|--:|
+||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)|
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
new file mode 100644
index 0000000000000000000000000000000000000000..55fcd5f00f28ca62b561f64b4f634c2dc3e2f0d2
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -0,0 +1,190 @@
+# GitLab Pages from A to Z: Part 3
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- **Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates**
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## Setting Up Custom Domains - DNS Records and SSL/TLS Certificates
+
+As described in the previous part of this series, setting up GitLab Pages with custom domains, and adding SSL/TLS certificates to them, are optional features of GitLab Pages.
+
+These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`.
+
+### DNS Records
+
+A Domain Name System (DNS) web service routes visitors to websites
+by translating domain names (such as `www.example.com`) into the
+numeric IP addresses (such as `192.0.2.1`) that computers use to
+connect to each other.
+
+A DNS record is created to point a (sub)domain to a certain location,
+which can be an IP address or another domain. In case you want to use
+GitLab Pages with your own (sub)domain, you need to access your domain's
+registrar control panel to add a DNS record pointing it back to your
+GitLab Pages site.
+
+Note that **how to** add DNS records depends on which server your domain
+is hosted on. Every control panel has its own place to do it. If you are
+not an admin of your domain, and don't have access to your registrar,
+you'll need to ask for the technical support of your hosting service
+to do it for you.
+
+To help you out, we've gathered some instructions on how to do that
+for the most popular hosting services:
+
+- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html)
+- [Bluehost](https://my.bluehost.com/cgi/help/559)
+- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-)
+- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone)
+- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-)
+- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238)
+- [Hostgator](http://support.hostgator.com/articles/changing-dns-records)
+- [Inmotion hosting](https://my.bluehost.com/cgi/help/559)
+- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain)
+- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx)
+
+If your hosting service is not listed above, you can just try to
+search the web for "how to add dns record on <my hosting service>".
+
+#### DNS A record
+
+In case you want to point a root domain (`example.com`) to your
+GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
+log into your domain's admin control panel and add a DNS `A` record
+pointing your domain to Pages' server IP address. For projects on
+GitLab.com, this IP is `52.167.214.135`. For projects leaving in
+other GitLab instances (CE or EE), please contact your sysadmin
+asking for this information (which IP address is Pages server
+running on your instance).
+
+**Practical Example:**
+
+![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated.png)
+
+#### DNS CNAME record
+
+In case you want to point a subdomain (`hello-world.example.com`)
+to your GitLab Pages site initially deployed to `namespace.gitlab.io`,
+you need to log into your domain's admin control panel and add a DNS
+`CNAME` record pointing your subdomain to your website URL
+(`namespace.gitlab.io`) address.
+
+Notice that, despite it's a user or project website, the `CNAME`
+should point to your Pages domain (`namespace.gitlab.io`),
+without any `/project-name`.
+
+**Practical Example:**
+
+![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png)
+
+#### TL;DR
+
+| From | DNS Record | To |
+| ---- | ---------- | -- |
+| domain.com | A | 52.167.214.135 |
+| subdomain.domain.com | CNAME | namespace.gitlab.io |
+
+> **Notes**:
+>
+> - **Do not** use a CNAME record if you want to point your
+`domain.com` to your GitLab Pages site. Use an `A` record instead.
+> - **Do not** add any special chars after the default Pages
+domain. E.g., **do not** point your `subdomain.domain.com` to
+`namespace.gitlab.io.` or `namespace.gitlab.io/`.
+> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`.
+
+### SSL/TLS Certificates
+
+Every GitLab Pages project on GitLab.com will be available under
+HTTPS for the default Pages domain (`*.gitlab.io`). Once you set
+up your Pages project with your custom (sub)domain, if you want
+it secured by HTTPS, you will have to issue a certificate for that
+(sub)domain and install it on your project.
+
+>**Note:**
+Certificates are NOT required to add to your custom
+(sub)domain on your GitLab Pages project, though they are
+highly recommendable.
+
+The importance of having any website securely served under HTTPS
+is explained on the introductory section of the blog post
+[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview).
+
+The reason why certificates are so important is that they encrypt
+the connection between the **client** (you, me, your visitors)
+and the **server** (where you site lives), through a keychain of
+authentications and validations.
+
+### Issuing Certificates
+
+GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by
+[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority)
+and self-signed certificates. Of course,
+[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate),
+for security reasons and for having browsers trusting your
+site's certificate.
+
+There are several different kinds of certificates, each one
+with certain security level. A static personal website will
+not require the same security level as an online banking web app,
+for instance. There are a couple Certificate Authorities that
+offer free certificates, aiming to make the internet more secure
+to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/),
+which issues certificates trusted by most of browsers, it's open
+source, and free to use. Please read through this tutorial to
+understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/).
+
+With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/),
+which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/).
+Their certs are valid up to 15 years. Read through the tutorial on
+[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
+
+### Adding certificates to your project
+
+Regardless the CA you choose, the steps to add your certificate to
+your Pages project are the same.
+
+#### What do you need
+
+1. A PEM certificate
+1. An intermediate certificate
+1. A public key
+
+![Pages project - adding certificates](img/add_certificate_to_pages.png)
+
+These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**.
+
+#### What's what?
+
+- A PEM certificate is the certificate generated by the CA,
+which needs to be added to the field **Certificate (PEM)**.
+- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is
+the part of the encryption keychain that identifies the CA.
+Usually it's combined with the PEM certificate, but there are
+some cases in which you need to add them manually.
+[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+are one of these cases.
+- A public key is an encrypted key which validates
+your PEM against your domain.
+
+#### Now what?
+
+Now that you hopefully understand why you need all
+of this, it's simple:
+
+- Your PEM certificate needs to be added to the first field
+- If your certificate is missing its intermediate, copy
+and paste the root certificate (usually available from your CA website)
+and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
+just jumping a line between them.
+- Copy your public key and paste it in the last field
+
+>**Note:**
+**Do not** open certificates or encryption keys in
+regular text editors. Always use code editors (such as
+Sublime Text, Atom, Dreamweaver, Brackets, etc).
+
+|||
+|:--|--:|
+|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|[**Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_four.md)|
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
new file mode 100644
index 0000000000000000000000000000000000000000..d0e2c467fee7c7a37b210455d87dff352d6a3428
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -0,0 +1,154 @@
+# GitLab Pages from A to Z: Part 2
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- **Part 2: Quick start guide - Setting up GitLab Pages**
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## Setting up GitLab Pages
+
+For a complete step-by-step tutorial, please read the
+blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain
+what do you need and why do you need them.
+
+## What you need to get started
+
+1. A project
+1. A configuration file (`.gitlab-ci.yml`) to deploy your site
+1. A specific `job` called `pages` in the configuration file
+that will make GitLab aware that you are deploying a GitLab Pages website
+
+Optional Features:
+
+1. A custom domain or subdomain
+1. A DNS pointing your (sub)domain to your Pages site
+   1. **Optional**: an SSL/TLS certificate so your custom
+   domain is accessible under HTTPS.
+
+The optional settings, custom domain, DNS records, and SSL/TLS certificates, are described in [Part 3](getting_started_part_three.md)).
+
+## Project
+
+Your GitLab Pages project is a regular project created the
+same way you do for the other ones. To get started with GitLab Pages, you have two ways:
+
+- Fork one of the templates from Page Examples, or
+- Create a new project from scratch
+
+Let's go over both options.
+
+### Fork a project to get started from
+
+To make things easy for you, we've created this
+[group](https://gitlab.com/pages) of default projects
+containing the most popular SSGs templates.
+
+Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've
+created for the steps below.
+
+1. Choose your SSG template
+1. Fork a project from the [Pages group](https://gitlab.com/pages)
+1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project**
+
+    ![remove fork relashionship](img/remove_fork_relashionship.png)
+
+1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines**
+1. Trigger a build (push a change to any file)
+1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages**
+
+To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to:
+
+- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository**
+- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file.
+
+> **Notes:**
+>
+>1. Why do I need to remove the fork relationship?
+>
+>     Unless you want to contribute to the original project,
+you won't need it connected to the upstream. A
+[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork)
+is useful for submitting merge requests to the upstream.
+>
+> 2. Why do I need to enable Shared Runners?
+>
+>     Shared Runners will run the script set by your GitLab CI
+configuration file. They're enabled by default to new projects,
+but not to forks.
+
+### Create a project from scratch
+
+1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
+click **New project**, and name it considering the
+[practical examples](getting_started_part_one.md#practical-examples).
+1. Clone it to your local computer, add your website
+files to your project, add, commit and push to GitLab.
+1. From the your **Project**'s page, click **Set up CI**:
+
+    ![setup GitLab CI](img/setup_ci.png)
+
+1. Choose one of the templates from the dropbox menu.
+Pick up the template corresponding to the SSG you're using (or plain HTML).
+
+    ![gitlab-ci templates](img/choose_ci_template.png)
+
+Once you have both site files and `.gitlab-ci.yml` in your project's
+root, GitLab CI will build your site and deploy it with Pages.
+Once the first build passes, you see your site is live by
+navigating to your **Project**'s **Settings** > **Pages**,
+where you'll find its default URL.
+
+> **Notes:**
+>
+> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
+if you don't find yours among the templates, you'll need
+to configure your own `.gitlab-ci.yml`. Do do that, please
+read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among
+the [example projects](https://gitlab.com/pages). If you set
+up a new one, please
+[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md)
+to our examples.
+>
+> - The second step _"Clone it to your local computer"_, can be done
+differently, achieving the same results: instead of cloning the bare
+repository to you local computer and moving your site files into it,
+you can run `git init` in your local website directory, add the
+remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
+then add, commit, and push.
+
+### URLs and Baseurls
+
+Every Static Site Generator (SSG) default configuration expects
+to find your website under a (sub)domain (`example.com`), not
+in a subdirectory of that domain (`example.com/subdir`). Therefore,
+whenever you publish a project website (`namespace.gitlab.io/project-name`),
+you'll have to look for this configuration (base URL) on your SSG's
+documentation and set it up to reflect this pattern.
+
+For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
+configuration file, `_config.yml`. If your website URL is
+`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
+
+```yaml
+baseurl: "/blog"
+```
+
+On the contrary, if you deploy your website after forking one of
+our [default examples](https://gitlab.com/pages), the baseurl will
+already be configured this way, as all examples there are project
+websites. If you decide to make yours a user or group website, you'll
+have to remove this configuration from your project. For the Jekyll
+example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
+
+```yaml
+baseurl: ""
+```
+
+### Custom Domains
+
+GitLab Pages supports custom domains and subdomains, served under HTTPS or HTTPS.
+Please check the [next part](getting_started_part_three.md) of this series for an overview.
+
+|||
+|:--|--:|
+|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Setting Up Custom Domains - DNS Records and SSL/TLS Certificates →**](getting_started_part_three.md)|
diff --git a/doc/user/project/pages/img/add_certificate_to_pages.png b/doc/user/project/pages/img/add_certificate_to_pages.png
new file mode 100644
index 0000000000000000000000000000000000000000..d92a981dc6037962f043956be97ecc5e45378869
Binary files /dev/null and b/doc/user/project/pages/img/add_certificate_to_pages.png differ
diff --git a/doc/user/project/pages/img/choose_ci_template.png b/doc/user/project/pages/img/choose_ci_template.png
new file mode 100644
index 0000000000000000000000000000000000000000..0697542abc863152a37e1ebe3b25a7beb5c773fc
Binary files /dev/null and b/doc/user/project/pages/img/choose_ci_template.png differ
diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png
new file mode 100644
index 0000000000000000000000000000000000000000..2661a497b91f785210a379ac26562c4e97a9e7cb
Binary files /dev/null and b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png differ
diff --git a/doc/user/project/pages/img/dns_cname_record_example.png b/doc/user/project/pages/img/dns_cname_record_example.png
new file mode 100644
index 0000000000000000000000000000000000000000..43d1a8385440fecd04dfb87a4fbfcec9de2f445e
Binary files /dev/null and b/doc/user/project/pages/img/dns_cname_record_example.png differ
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
index a936d8e5dbde3825a8898692944092c49de85733..be47f9d2a443380bcad43332309090e1da3970cf 100644
Binary files a/doc/user/project/pages/img/pages_create_project.png and b/doc/user/project/pages/img/pages_create_project.png differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
index 3f615d3757d2276e25268d9d2af8493d4c343946..2f1a19ae424e0d4dd02042a3852f5e2ac23e60ac 100644
Binary files a/doc/user/project/pages/img/pages_create_user_page.png and b/doc/user/project/pages/img/pages_create_user_page.png differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
index 8d34f3b7f386d2b51eac5c4312eddf82da875337..274e98fde4dbf8f9b9cf8436d046d44fde9a60e3 100644
Binary files a/doc/user/project/pages/img/pages_dns_details.png and b/doc/user/project/pages/img/pages_dns_details.png differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
index 2bc7cee07a609f54f4f5e308c7c12547309a5b55..6bc92db6b4149621cafcd0e5d356a54f44175b6c 100644
Binary files a/doc/user/project/pages/img/pages_multiple_domains.png and b/doc/user/project/pages/img/pages_multiple_domains.png differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
index c3640133bb266a6eefbf9d491a8dd74e3a3000ac..cd59defa006265cd6d1d0d73f8592f9e41bccfe3 100644
Binary files a/doc/user/project/pages/img/pages_new_domain_button.png and b/doc/user/project/pages/img/pages_new_domain_button.png differ
diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png
index adbfb654877d8dede23e21405c3dfce87ed09e5e..b064310380e500db9b6e643c5fa640dd79d0e96c 100644
Binary files a/doc/user/project/pages/img/pages_remove.png and b/doc/user/project/pages/img/pages_remove.png differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
index 06d85ab197186e06428de2466aede378e656f253..dc431ea3fef52686e5ef7893a5f6f8ea1fc2579f 100644
Binary files a/doc/user/project/pages/img/pages_upload_cert.png and b/doc/user/project/pages/img/pages_upload_cert.png differ
diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relashionship.png
new file mode 100644
index 0000000000000000000000000000000000000000..67c45491f081cd6f942fae69fe92cadaf3023ddc
Binary files /dev/null and b/doc/user/project/pages/img/remove_fork_relashionship.png differ
diff --git a/doc/user/project/pages/img/setup_ci.png b/doc/user/project/pages/img/setup_ci.png
new file mode 100644
index 0000000000000000000000000000000000000000..214c1cc668f3b30e3c936197244dc1a593664632
Binary files /dev/null and b/doc/user/project/pages/img/setup_ci.png differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 4c4f15aad402c2debc9eaa10d4aec21eceb96481..abe6b4cbd8eca7af20303a024b7d164f059db3c2 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,437 +1,49 @@
-# GitLab Pages
-
-> **Notes:**
-> - This feature was [introduced][ee-80] in GitLab EE 8.3.
-> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
-> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
-> - This document is about the user guide. To learn how to enable GitLab Pages
->   across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
-
-With GitLab Pages you can host for free your static websites on GitLab.
-Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
-deploy static pages for your individual projects, your user or your group.
-
-Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
-information, if you are using GitLab.com to host your website.
-
-## Getting started with GitLab Pages
-
-> **Note:**
-> In the rest of this document we will assume that the general domain name that
-> is used for GitLab Pages is `example.io`.
-
-In general there are two types of pages one might create:
-
-- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
-- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
-
-In GitLab, usernames and groupnames are unique and we often refer to them
-as namespaces. There can be only one namespace in a GitLab instance. Below you
-can see the connection between the type of GitLab Pages, what the project name
-that is created on GitLab looks like and the website URL it will be ultimately
-be served on.
-
-| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
-| -------------------- | ------------ | ----------- |
-| User pages  | `username.example.io`  | `http(s)://username.example.io`  |
-| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
-| Project pages owned by a user  | `projectname` | `http(s)://username.example.io/projectname` |
-| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
-
-> **Warning:**
-> There are some known [limitations](#limitations) regarding namespaces served
-> under the general domain name and HTTPS. Make sure to read that section.
-
-### GitLab Pages requirements
-
-In brief, this is what you need to upload your website in GitLab Pages:
-
-1. Find out the general domain name that is used for GitLab Pages
-   (ask your administrator). This is very important, so you should first make
-   sure you get that right.
-1. Create a project
-1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
-   of your repository with a specific job named [`pages`][pages]
-1. Set up a GitLab Runner to build your website
-
-> **Note:**
-If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab
-administrator, you should be able to use them instead of bringing your own.
-
-### User or group Pages
-
-For user and group pages, the name of the project should be specific to the
-username or groupname and the general domain name that is used for GitLab Pages.
-Head over your GitLab instance that supports GitLab Pages and create a
-repository named `username.example.io`, where `username` is your username on
-GitLab. If the first part of the project name doesn't match exactly your
-username, it won’t work, so make sure to get it right.
-
-To create a group page, the steps are the same like when creating a website for
-users. Just make sure that you are creating the project within the group's
-namespace.
-
-![Create a user-based pages project](img/pages_create_user_page.png)
-
----
-
-After you push some static content to your repository and GitLab Runner uploads
-the artifacts to GitLab CI, you will be able to access your website under
-`http(s)://username.example.io`. Keep reading to find out how.
-
->**Note:**
-If your username/groupname contains a dot, for example `foo.bar`, you will not
-be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
-
-### Project Pages
-
-GitLab Pages for projects can be created by both user and group accounts.
-The steps to create a project page for a user or a group are identical:
-
-1. Create a new project
-1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
-   of your repository with a specific job named [`pages`][pages].
-1. Set up a GitLab Runner to build your website
-
-A user's project will be served under `http(s)://username.example.io/projectname`
-whereas a group's project under `http(s)://groupname.example.io/projectname`.
-
-### Explore the contents of `.gitlab-ci.yml`
-
-The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
-gives you absolute control over the build process. You can actually watch your
-website being built live by following the CI job traces.
-
-> **Note:**
-> Before reading this section, make sure you familiarize yourself with GitLab CI
-> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
-> following our [quick start guide].
-
-To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
-rules below:
-
-1. A special job named [`pages`][pages] must be defined
-1. Any static content which will be served by GitLab Pages must be placed under
-   a `public/` directory
-1. `artifacts` with a path to the `public/` directory must be defined
-
-In its simplest form, `.gitlab-ci.yml` looks like:
-
-```yaml
-pages:
-  script:
-  - my_commands
-  artifacts:
-    paths:
-    - public
-```
-
-When the Runner reaches to build the `pages` job, it executes whatever is
-defined in the `script` parameter and if the job completes with a non-zero
-exit status, it then uploads the `public/` directory to GitLab Pages.
-
-The `public/` directory should contain all the static content of your website.
-Depending on how you plan to publish your website, the steps defined in the
-[`script` parameter](../../../ci/yaml/README.md#script) may differ.
-
-Be aware that Pages are by default branch/tag agnostic and their deployment
-relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
-`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
-whenever a new commit is pushed to whatever branch or tag, the Pages will be
-overwritten. In the example below, we limit the Pages to be deployed whenever
-a commit is pushed only on the `master` branch:
-
-```yaml
-pages:
-  script:
-  - my_commands
-  artifacts:
-    paths:
-    - public
-  only:
-  - master
-```
-
-We then tell the Runner to treat the `public/` directory as `artifacts` and
-upload it to GitLab. And since all these parameters were all under a `pages`
-job, the contents of the `public` directory will be served by GitLab Pages.
-
-#### How `.gitlab-ci.yml` looks like when the static content is in your repository
-
-Supposedly your repository contained the following files:
-
-```
-├── index.html
-├── css
-│   └── main.css
-└── js
-    └── main.js
-```
-
-Then the `.gitlab-ci.yml` example below simply moves all files from the root
-directory of the project to the `public/` directory. The `.public` workaround
-is so `cp` doesn't also copy `public/` to itself in an infinite loop:
-
-```yaml
-pages:
-  script:
-  - mkdir .public
-  - cp -r * .public
-  - mv .public public
-  artifacts:
-    paths:
-    - public
-  only:
-  - master
-```
-
-#### How `.gitlab-ci.yml` looks like when using a static generator
-
-In general, GitLab Pages support any kind of [static site generator][staticgen],
-since `.gitlab-ci.yml` can be configured to run any possible command.
-
-In the root directory of your Git repository, place the source files of your
-favorite static generator. Then provide a `.gitlab-ci.yml` file which is
-specific to your static generator.
-
-The example below, uses [Jekyll] to build the static site:
-
-```yaml
-image: ruby:2.1             # the script will run in Ruby 2.1 using the Docker image ruby:2.1
-
-pages:                      # the build job must be named pages
-  script:
-  - gem install jekyll      # we install jekyll
-  - jekyll build -d public/ # we tell jekyll to build the site for us
-  artifacts:
-    paths:
-    - public                # this is where the site will live and the Runner uploads it in GitLab
-  only:
-  - master                  # this script is only affecting the master branch
-```
-
-Here, we used the Docker executor and in the first line we specified the base
-image against which our jobs will run.
-
-You have to make sure that the generated static files are ultimately placed
-under the `public` directory, that's why in the `script` section we run the
-`jekyll` command that jobs the website and puts all content in the `public/`
-directory. Depending on the static generator of your choice, this command will
-differ. Search in the documentation of the static generator you will use if
-there is an option to explicitly set the output directory. If there is not
-such an option, you can always add one more line under `script` to rename the
-resulting directory in `public/`.
-
-We then tell the Runner to treat the `public/` directory as `artifacts` and
-upload it to GitLab.
-
----
-
-See the [jekyll example project][pages-jekyll] to better understand how this
-works.
-
-For a list of Pages projects, see the [example projects](#example-projects) to
-get you started.
-
-#### How to set up GitLab Pages in a repository where there's also actual code
-
-Remember that GitLab Pages are by default branch/tag agnostic and their
-deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
-the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
-whenever a new commit is pushed to a branch that will be used specifically for
-your pages.
-
-That way, you can have your project's code in the `master` branch and use an
-orphan branch (let's name it `pages`) that will host your static generator site.
-
-You can create a new empty branch like this:
-
-```bash
-git checkout --orphan pages
-```
-
-The first commit made on this new branch will have no parents and it will be
-the root of a new history totally disconnected from all the other branches and
-commits. Push the source files of your static generator in the `pages` branch.
-
-Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
-one, specifying to execute everything in the `pages` branch:
-
-```
-image: ruby:2.1
-
-pages:
-  script:
-  - gem install jekyll
-  - jekyll build -d public/
-  artifacts:
-    paths:
-    - public
-  only:
-  - pages
-```
-
-See an example that has different files in the [`master` branch][jekyll-master]
-and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
-also includes `.gitlab-ci.yml`.
-
-[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
-[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
-
-## Next steps
-
-So you have successfully deployed your website, congratulations! Let's check
-what more you can do with GitLab Pages.
-
-### Example projects
-
-Below is a list of example projects for GitLab Pages with a plain HTML website
-or various static site generators. Contributions are very welcome.
-
-- [Plain HTML](https://gitlab.com/pages/plain-html)
-- [Jekyll](https://gitlab.com/pages/jekyll)
-- [Hugo](https://gitlab.com/pages/hugo)
-- [Middleman](https://gitlab.com/pages/middleman)
-- [Hexo](https://gitlab.com/pages/hexo)
-- [Brunch](https://gitlab.com/pages/brunch)
-- [Metalsmith](https://gitlab.com/pages/metalsmith)
-- [Harp](https://gitlab.com/pages/harp)
-
-Visit the GitLab Pages group for a full list of example projects:
-<https://gitlab.com/groups/pages>.
-
-### Add a custom domain to your Pages website
-
-If this setting is enabled by your GitLab administrator, you should be able to
-see the **New Domain** button when visiting your project's settings through the
-gear icon in the top right and then navigating to **Pages**.
-
-![New domain button](img/pages_new_domain_button.png)
-
----
-
-You can add multiple domains pointing to your website hosted under GitLab.
-Once the domain is added, you can see it listed under the **Domains** section.
-
-![Pages multiple domains](img/pages_multiple_domains.png)
-
----
-
-As a last step, you need to configure your DNS and add a CNAME pointing to your
-user/group page. Click on the **Details** button of a domain for further
-instructions.
-
-![Pages DNS details](img/pages_dns_details.png)
-
----
-
->**Note:**
-Currently there is support only for custom domains on per-project basis. That
-means that if you add a custom domain (`example.com`) for your user website
-(`username.example.io`), a project that is served under `username.example.io/foo`,
-will not be accessible under `example.com/foo`.
-
-### Secure your custom domain website with TLS
-
-When you add a new custom domain, you also have the chance to add a TLS
-certificate. If this setting is enabled by your GitLab administrator, you
-should be able to see the option to upload the public certificate and the
-private key when adding a new domain.
-
-![Pages upload cert](img/pages_upload_cert.png)
-
-### Custom error codes pages
-
-You can provide your own 403 and 404 error pages by creating the `403.html` and
-`404.html` files respectively in the root directory of the `public/` directory
-that will be included in the artifacts. Usually this is the root directory of
-your project, but that may differ depending on your static generator
-configuration.
-
-If the case of `404.html`, there are different scenarios. For example:
-
-- If you use project Pages (served under `/projectname/`) and try to access
-  `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
-  `/projectname/404.html`, and then `/404.html`.
-- If you use user/group Pages (served under `/`) and try to access
-  `/non/existing_file` GitLab Pages will try to serve `/404.html`.
-- If you use a custom domain and try to access `/non/existing_file`, GitLab
-  Pages will try to serve only `/404.html`.
-
-### Remove the contents of your pages
-
-If you ever feel the need to purge your Pages content, you can do so by going
-to your project's settings through the gear icon in the top right, and then
-navigating to **Pages**. Hit the **Remove pages** button and your Pages website
-will be deleted. Simple as that.
-
-![Remove pages](img/pages_remove.png)
-
-## GitLab Pages on GitLab.com
-
-If you are using GitLab.com to host your website, then:
-
-- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
-- Custom domains and TLS support are enabled.
-- Shared runners are enabled by default, provided for free and can be used to
-  build your website. If you want you can still bring your own Runner.
-
-The rest of the guide still applies.
-
-## Limitations
-
-When using Pages under the general domain of a GitLab instance (`*.example.io`),
-you _cannot_ use HTTPS with sub-subdomains. That means that if your
-username/groupname contains a dot, for example `foo.bar`, the domain
-`https://foo.bar.example.io` will _not_ work. This is a limitation of the
-[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
-don't redirect HTTP to HTTPS.
-
-[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
-
-## Redirects in GitLab Pages
-
-Since you cannot use any custom server configuration files, like `.htaccess` or
-any `.conf` file for that matter, if you want to redirect a web page to another
-location, you can use the [HTTP meta refresh tag][metarefresh].
-
-Some static site generators provide plugins for that functionality so that you
-don't have to create and edit HTML files manually. For example, Jekyll has the
-[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
-
-## Frequently Asked Questions
-
-### Can I download my generated pages?
-
-Sure. All you need to do is download the artifacts archive from the job page.
-
-### Can I use GitLab Pages if my project is private?
-
-Yes. GitLab Pages don't care whether you set your project's visibility level
-to private, internal or public.
-
-### Do I need to create a user/group website before creating a project website?
-
-No, you don't. You can create your project first and it will be accessed under
-`http(s)://namespace.example.io/projectname`.
-
-## Known issues
-
-For a list of known issues, visit GitLab's [public issue tracker].
-
----
-
-[jekyll]: http://jekyllrb.com/
-[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
-[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
-[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
-[gitlab ci]: https://about.gitlab.com/gitlab-ci
-[gitlab runner]: https://docs.gitlab.com/runner
-[pages]: ../../../ci/yaml/README.md#pages
-[yaml]: ../../../ci/yaml/README.md
-[staticgen]: https://www.staticgen.com/
-[pages-jekyll]: https://gitlab.com/pages/jekyll
-[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
-[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages
-[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
-[quick start guide]: ../../../ci/quick_start/README.md
+# GitLab Pages documentation
+
+With GitLab Pages you can create static websites for your GitLab projects,
+groups, or user accounts. You can use any static website generator: Jekyll,
+Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains
+as you like and bring your own TLS certificate to secure them.
+
+Here's some info we've gathered to get you started.
+
+## General info
+
+- [Product webpage](https://pages.gitlab.io)
+- ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
+- [Pages group - templates](https://gitlab.com/pages)
+- [General user documentation](introduction.md)
+- [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md)
+- ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
+
+## Getting started
+
+- **GitLab Pages from A to Z**
+  - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+  - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+  - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+  - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+- **Static Site Generators - Blog posts series**
+  - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+  - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/)
+  - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- **Secure GitLab Pages custom domain with SSL/TLS certificates**
+  - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/)
+  - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+  - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+- **General**
+  - [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide
+  - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/)
+
+## Video tutorials
+
+- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg)
+- [How to Enable GitLab Pages for GitLab CE and EE (for Admins only)](https://youtu.be/dD8c7WNcc6s)
+
+## Advanced use
+
+- **Blog Posts**
+  - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+  - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+  - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+  - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
new file mode 100644
index 0000000000000000000000000000000000000000..deaceabb7c5ef4819b74b8e1cd601ace319c0438
--- /dev/null
+++ b/doc/user/project/pages/introduction.md
@@ -0,0 +1,447 @@
+# GitLab Pages
+
+> **Notes:**
+> - This feature was [introduced][ee-80] in GitLab EE 8.3.
+> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - This document is about the user guide. To learn how to enable GitLab Pages
+>   across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
+
+With GitLab Pages you can host for free your static websites on GitLab.
+Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
+deploy static pages for your individual projects, your user or your group.
+
+Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
+information, if you are using GitLab.com to host your website.
+
+Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials).
+
+## Getting started with GitLab Pages
+
+> **Note:**
+> In the rest of this document we will assume that the general domain name that
+> is used for GitLab Pages is `example.io`.
+
+In general there are two types of pages one might create:
+
+- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
+- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+
+In GitLab, usernames and groupnames are unique and we often refer to them
+as namespaces. There can be only one namespace in a GitLab instance. Below you
+can see the connection between the type of GitLab Pages, what the project name
+that is created on GitLab looks like and the website URL it will be ultimately
+be served on.
+
+| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
+| -------------------- | ------------ | ----------- |
+| User pages  | `username.example.io`  | `http(s)://username.example.io`  |
+| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
+| Project pages owned by a user  | `projectname` | `http(s)://username.example.io/projectname` |
+| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
+
+> **Warning:**
+> There are some known [limitations](#limitations) regarding namespaces served
+> under the general domain name and HTTPS. Make sure to read that section.
+
+### GitLab Pages requirements
+
+In brief, this is what you need to upload your website in GitLab Pages:
+
+1. Find out the general domain name that is used for GitLab Pages
+   (ask your administrator). This is very important, so you should first make
+   sure you get that right.
+1. Create a project
+1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
+   of your repository with a specific job named [`pages`][pages]
+1. Set up a GitLab Runner to build your website
+
+> **Note:**
+If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab
+administrator, you should be able to use them instead of bringing your own.
+
+### User or group Pages
+
+For user and group pages, the name of the project should be specific to the
+username or groupname and the general domain name that is used for GitLab Pages.
+Head over your GitLab instance that supports GitLab Pages and create a
+repository named `username.example.io`, where `username` is your username on
+GitLab. If the first part of the project name doesn't match exactly your
+username, it won’t work, so make sure to get it right.
+
+To create a group page, the steps are the same like when creating a website for
+users. Just make sure that you are creating the project within the group's
+namespace.
+
+![Create a user-based pages project](img/pages_create_user_page.png)
+
+---
+
+After you push some static content to your repository and GitLab Runner uploads
+the artifacts to GitLab CI, you will be able to access your website under
+`http(s)://username.example.io`. Keep reading to find out how.
+
+>**Note:**
+If your username/groupname contains a dot, for example `foo.bar`, you will not
+be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
+
+### Project Pages
+
+GitLab Pages for projects can be created by both user and group accounts.
+The steps to create a project page for a user or a group are identical:
+
+1. Create a new project
+1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
+   of your repository with a specific job named [`pages`][pages].
+1. Set up a GitLab Runner to build your website
+
+A user's project will be served under `http(s)://username.example.io/projectname`
+whereas a group's project under `http(s)://groupname.example.io/projectname`.
+
+## Quick Start
+
+Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on
+[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork].
+
+See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages.
+
+### Explore the contents of `.gitlab-ci.yml`
+
+The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
+gives you absolute control over the build process. You can actually watch your
+website being built live by following the CI job traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
+> following our [quick start guide].
+
+To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
+rules below:
+
+1. A special job named [`pages`][pages] must be defined
+1. Any static content which will be served by GitLab Pages must be placed under
+   a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+In its simplest form, `.gitlab-ci.yml` looks like:
+
+```yaml
+pages:
+  script:
+  - my_commands
+  artifacts:
+    paths:
+    - public
+```
+
+When the Runner reaches to build the `pages` job, it executes whatever is
+defined in the `script` parameter and if the job completes with a non-zero
+exit status, it then uploads the `public/` directory to GitLab Pages.
+
+The `public/` directory should contain all the static content of your website.
+Depending on how you plan to publish your website, the steps defined in the
+[`script` parameter](../../../ci/yaml/README.md#script) may differ.
+
+Be aware that Pages are by default branch/tag agnostic and their deployment
+relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
+`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to whatever branch or tag, the Pages will be
+overwritten. In the example below, we limit the Pages to be deployed whenever
+a commit is pushed only on the `master` branch:
+
+```yaml
+pages:
+  script:
+  - my_commands
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+```
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab. And since all these parameters were all under a `pages`
+job, the contents of the `public` directory will be served by GitLab Pages.
+
+#### How `.gitlab-ci.yml` looks like when the static content is in your repository
+
+Supposedly your repository contained the following files:
+
+```
+├── index.html
+├── css
+│   └── main.css
+└── js
+    └── main.js
+```
+
+Then the `.gitlab-ci.yml` example below simply moves all files from the root
+directory of the project to the `public/` directory. The `.public` workaround
+is so `cp` doesn't also copy `public/` to itself in an infinite loop:
+
+```yaml
+pages:
+  script:
+  - mkdir .public
+  - cp -r * .public
+  - mv .public public
+  artifacts:
+    paths:
+    - public
+  only:
+  - master
+```
+
+#### How `.gitlab-ci.yml` looks like when using a static generator
+
+In general, GitLab Pages support any kind of [static site generator][staticgen],
+since `.gitlab-ci.yml` can be configured to run any possible command.
+
+In the root directory of your Git repository, place the source files of your
+favorite static generator. Then provide a `.gitlab-ci.yml` file which is
+specific to your static generator.
+
+The example below, uses [Jekyll] to build the static site:
+
+```yaml
+image: ruby:2.1             # the script will run in Ruby 2.1 using the Docker image ruby:2.1
+
+pages:                      # the build job must be named pages
+  script:
+  - gem install jekyll      # we install jekyll
+  - jekyll build -d public/ # we tell jekyll to build the site for us
+  artifacts:
+    paths:
+    - public                # this is where the site will live and the Runner uploads it in GitLab
+  only:
+  - master                  # this script is only affecting the master branch
+```
+
+Here, we used the Docker executor and in the first line we specified the base
+image against which our jobs will run.
+
+You have to make sure that the generated static files are ultimately placed
+under the `public` directory, that's why in the `script` section we run the
+`jekyll` command that jobs the website and puts all content in the `public/`
+directory. Depending on the static generator of your choice, this command will
+differ. Search in the documentation of the static generator you will use if
+there is an option to explicitly set the output directory. If there is not
+such an option, you can always add one more line under `script` to rename the
+resulting directory in `public/`.
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab.
+
+---
+
+See the [jekyll example project][pages-jekyll] to better understand how this
+works.
+
+For a list of Pages projects, see the [example projects](#example-projects) to
+get you started.
+
+#### How to set up GitLab Pages in a repository where there's also actual code
+
+Remember that GitLab Pages are by default branch/tag agnostic and their
+deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
+the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to a branch that will be used specifically for
+your pages.
+
+That way, you can have your project's code in the `master` branch and use an
+orphan branch (let's name it `pages`) that will host your static generator site.
+
+You can create a new empty branch like this:
+
+```bash
+git checkout --orphan pages
+```
+
+The first commit made on this new branch will have no parents and it will be
+the root of a new history totally disconnected from all the other branches and
+commits. Push the source files of your static generator in the `pages` branch.
+
+Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
+one, specifying to execute everything in the `pages` branch:
+
+```
+image: ruby:2.1
+
+pages:
+  script:
+  - gem install jekyll
+  - jekyll build -d public/
+  artifacts:
+    paths:
+    - public
+  only:
+  - pages
+```
+
+See an example that has different files in the [`master` branch][jekyll-master]
+and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+also includes `.gitlab-ci.yml`.
+
+[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
+[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
+
+## Next steps
+
+So you have successfully deployed your website, congratulations! Let's check
+what more you can do with GitLab Pages.
+
+### Example projects
+
+Below is a list of example projects for GitLab Pages with a plain HTML website
+or various static site generators. Contributions are very welcome.
+
+- [Plain HTML](https://gitlab.com/pages/plain-html)
+- [Jekyll](https://gitlab.com/pages/jekyll)
+- [Hugo](https://gitlab.com/pages/hugo)
+- [Middleman](https://gitlab.com/pages/middleman)
+- [Hexo](https://gitlab.com/pages/hexo)
+- [Brunch](https://gitlab.com/pages/brunch)
+- [Metalsmith](https://gitlab.com/pages/metalsmith)
+- [Harp](https://gitlab.com/pages/harp)
+
+Visit the GitLab Pages group for a full list of example projects:
+<https://gitlab.com/groups/pages>.
+
+### Add a custom domain to your Pages website
+
+If this setting is enabled by your GitLab administrator, you should be able to
+see the **New Domain** button when visiting your project's settings through the
+gear icon in the top right and then navigating to **Pages**.
+
+![New domain button](img/pages_new_domain_button.png)
+
+---
+
+You can add multiple domains pointing to your website hosted under GitLab.
+Once the domain is added, you can see it listed under the **Domains** section.
+
+![Pages multiple domains](img/pages_multiple_domains.png)
+
+---
+
+As a last step, you need to configure your DNS and add a CNAME pointing to your
+user/group page. Click on the **Details** button of a domain for further
+instructions.
+
+![Pages DNS details](img/pages_dns_details.png)
+
+---
+
+>**Note:**
+Currently there is support only for custom domains on per-project basis. That
+means that if you add a custom domain (`example.com`) for your user website
+(`username.example.io`), a project that is served under `username.example.io/foo`,
+will not be accessible under `example.com/foo`.
+
+### Secure your custom domain website with TLS
+
+When you add a new custom domain, you also have the chance to add a TLS
+certificate. If this setting is enabled by your GitLab administrator, you
+should be able to see the option to upload the public certificate and the
+private key when adding a new domain.
+
+![Pages upload cert](img/pages_upload_cert.png)
+
+### Custom error codes pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+  `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
+  `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+  `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+  Pages will try to serve only `/404.html`.
+
+### Remove the contents of your pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted. Simple as that.
+
+![Remove pages](img/pages_remove.png)
+
+## GitLab Pages on GitLab.com
+
+If you are using GitLab.com to host your website, then:
+
+- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
+- Custom domains and TLS support are enabled.
+- Shared runners are enabled by default, provided for free and can be used to
+  build your website. If you want you can still bring your own Runner.
+
+The rest of the guide still applies.
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file for that matter, if you want to redirect a web page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## Frequently Asked Questions
+
+### Can I download my generated pages?
+
+Sure. All you need to do is download the artifacts archive from the job page.
+
+### Can I use GitLab Pages if my project is private?
+
+Yes. GitLab Pages don't care whether you set your project's visibility level
+to private, internal or public.
+
+### Do I need to create a user/group website before creating a project website?
+
+No, you don't. You can create your project first and it will be accessed under
+`http(s)://namespace.example.io/projectname`.
+
+## Known issues
+
+For a list of known issues, visit GitLab's [public issue tracker].
+
+[jekyll]: http://jekyllrb.com/
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[gitlab ci]: https://about.gitlab.com/gitlab-ci
+[gitlab runner]: https://docs.gitlab.com/runner/
+[pages]: ../../../ci/yaml/README.md#pages
+[yaml]: ../../../ci/yaml/README.md
+[staticgen]: https://www.staticgen.com/
+[pages-jekyll]: https://gitlab.com/pages/jekyll
+[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
+[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=pages
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[quick start guide]: ../../../ci/quick_start/README.md
+[pages-index-guide]: index.md
+[pages-quick]: getting_started_part_one.md
+[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index c415d566a7c5f6cc62ffc0a77d4ef68b14c20461..d47a3acdbe90c55471f54ae6dc518cb306da2141 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -109,12 +109,19 @@ the title of the issue and as suffix it will have its ID. Thus, the example
 screenshot above will yield a branch named
 `2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
 
+Since GitLab 9.0, when you click the `New branch` in an empty repository project, GitLab automatically creates the master branch, commits a blank `README.md` file to it and creates and redirects you to a new branch based on the issue title.
+If your [project is already configured with a deployment service][project-services-doc] (e.g. Kubernetes), GitLab takes one step further and prompts you to set up [auto deploy][auto-deploy-doc] by helping you create a `.gitlab-ci.yml` file.
+
+
 After the branch is created, you can edit files in the repository to fix
 the issue. When a merge request is created based on the newly created branch,
 the description field will automatically display the [issue closing pattern]
 `Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
 merge request is merged.
 
+[project-services-doc]: ../integrations/project_services.md
+[auto-deploy-doc]: ../../../ci/autodeploy/index.md
+
 ### Create a new branch from a project's dashboard
 
 If you want to make changes to several files before creating a new merge
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index be042ddf6236802d9903d5ee22be67acd486d65f..7a4f9f408f190d3479a8feb8876ba514c6825405 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -34,7 +34,7 @@ with all their related data and be moved into a new GitLab instance.
 | 8.10.0   | 0.1.2    |
 | 8.9.5    | 0.1.1    |
 | 8.9.0    | 0.1.0    |
- 
+
  > The table reflects what GitLab version we updated the Import/Export version at.
  > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
  > and the exports between them will be compatible.
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index ad5d51d34f21c7ca0703e7727e1187b2da2a6d8c..45176fde9db860568eae8c1e8fb75a84ced5c43d 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -35,3 +35,4 @@ do.
 | <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
 | `/remove_time_spent`       | Remove time spent |
 | `/target_branch <Branch Name>` | Set target branch for current merge request |
+| `/award :emoji:`  | Toggle award for :emoji: |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9e7ee47387ce51b918b943ba59fcd034bdce31fb..6a8de51a199f73469669a4ec78f5297b9e30a075 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -40,3 +40,4 @@
 - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
 - [Todos](todos.md)
 - [Snippets](../user/snippets.md)
+- [Subgroups](../user/group/subgroups/index.md)
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 1df0698afd016e4b6deb73ac9a2ac87e4b4faf1a..d74378cc564496496a7f64922d66c253c2879aca 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,65 +1 @@
-# Award emoji
-
->**Note:**
-[Introduced][1825] in GitLab 8.2.
-
-When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. Emoji can be awarded to issues and merge requests, making
-virtual celebrations easier.
-
-![Award emoji](img/award_emoji_select.png)
-
-Award emoji make it much easier to give and receive feedback without a long
-comment thread. Comments that are only emoji will automatically become
-award emoji.
-
-## Sort issues and merge requests on vote count
-
->**Note:**
-[Introduced][2871] in GitLab 8.5.
-
-You can quickly sort issues and merge requests by the number of votes they
-have received. The sort options can be found in the dropdown menu as "Most
-popular" and "Least popular".
-
-![Votes sort options](img/award_emoji_votes_sort_options.png)
-
----
-
-Sort by most popular issues/merge requests.
-
-![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
-
----
-
-Sort by least popular issues/merge requests.
-
-![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
-
----
-
-The total number of votes is not summed up. An issue with 18 upvotes and 5
-downvotes is considered more popular than an issue with 17 upvotes and no
-downvotes.
-
-## Award emoji for comments
-
->**Note:**
-[Introduced][4291] in GitLab 8.9.
-
-Award emoji can also be applied to individual comments when you want to
-celebrate an accomplishment or agree with an opinion.
-
-To add an award emoji, click the smile in the top right of the comment and pick
-an emoji from the dropdown.
-
-![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
-
-![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
-
-If you want to remove an award emoji, just click the emoji again and the vote
-will be removed.
-
-[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
-[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
-[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+This document was moved to [another location](../user/award_emojis.md).
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 4889e3ec50c279d72dfce3d6e3d82a7de44d01a8..d12c0c6d0c4de9003a4f0ca2d23b87ec31f29f6b 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -203,7 +203,7 @@ But the advantages of having stable identifiers outweigh this drawback.
 And to understand a change in context one can always look at the merge commit that groups all the commits together when the code is merged into the master branch.
 
 After you merge multiple commits from a feature branch into the master branch this is harder to undo.
-If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
+If you had squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
 Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
 This however, requires having specific merge commits for the commits your want to revert.
 If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise.
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
deleted file mode 100644
index e1b37beaf62bfc9ec3cfdcdab5ab2e7d6221701b..0000000000000000000000000000000000000000
Binary files a/doc/workflow/img/award_emoji_select.png and /dev/null differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
deleted file mode 100644
index 86ede4b0c100e6bc1aa29c6436aa776df7a1bab2..0000000000000000000000000000000000000000
Binary files a/doc/workflow/img/award_emoji_votes_least_popular.png and /dev/null differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
deleted file mode 100644
index 1d3e2e57aa0456e3a9a0a28384dd719f79533099..0000000000000000000000000000000000000000
Binary files a/doc/workflow/img/award_emoji_votes_most_popular.png and /dev/null differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
deleted file mode 100644
index c6dc1b939c1038e769b3fbe73129729b538c43ff..0000000000000000000000000000000000000000
Binary files a/doc/workflow/img/award_emoji_votes_sort_options.png and /dev/null differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 8c5020bee37cd6b1c94eafb22c804294d1caa720..6adde4479752f4ce2a9a7318a3fbb3ec9aa43590 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -4,13 +4,6 @@ Managing large files such as audio, video and graphics files has always been one
 of the shortcomings of Git. The general recommendation is to not have Git repositories
 larger than 1GB to preserve performance.
 
-GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html)
-(EE only), however in certain environments it is not always convenient to use
-different commands to differentiate between the large files and regular ones.
-
-Git LFS makes this simpler for the end user by removing the requirement to
-learn new commands.
-
 ## How it works
 
 Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
@@ -63,6 +56,12 @@ git commit -am "Added Debian iso"     # commit the file meta data
 git push origin master                # sync the git repo and large file to the GitLab server
 ```
 
+>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
+ LFS will not be working properly for people cloning the project.
+ ```bash
+ git add .gitattributes
+ ```
+
 Cloning the repository works the same as before. Git automatically detects the
 LFS-tracked files and clones them via HTTP. If you performed the git clone
 command with a SSH URL, you have to enter your GitLab credentials for HTTP
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
index dff36899aec54234aa4fedf72d92296246160ddd..37afe553e55a1c60cce6938bb857dcee1782f36b 100644
--- a/doc/workflow/milestones.md
+++ b/doc/workflow/milestones.md
@@ -1,13 +1,28 @@
 # Milestones
 
-Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date. 
+Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
 A common use is keeping track of an upcoming software version. Milestones are created per-project.
 
 ![milestone form](milestones/form.png)
 
 ## Groups and milestones
 
-You can create a milestone for several projects in the same group simultaneously. 
+You can create a milestone for several projects in the same group simultaneously.
 On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
 
 ![group milestone form](milestones/group_form.png)
+
+## Special milestone filters
+
+In addition to the milestones that exist in the project or group, there are some
+special options available when filtering by milestone:
+
+* **No Milestone** - only show issues or merge requests without a milestone.
+* **Upcoming** - show issues or merge request that belong to the next open
+  milestone with a due date, by project. (For example: if project A has
+  milestone v1 due in three days, and project B has milestone v2 due in a week,
+  then this will show issues or merge requests from milestone v1 in project A
+  and milestone v2 in project B.)
+* **Started** - show issues or merge requests from any milestone with a start
+  date less than today. Note that this can return results from several
+  milestones in the same project.
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2a5e622dc7d52c3bad4b19a509c3146d6cf9d4e2..7aa9b46081a9a37257ddf8372ef5600ffffb98a4 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -47,7 +47,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
 | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
 | <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
 | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>g</kbd> | Go to graphs |
+| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
 | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
 | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
 | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 92061dac7f446ba25839cbe791c136b74be935f8..b1d5e4a7acbf707a3625b49ab938ee8143d29a69 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,6 +11,7 @@ Feature: Dashboard
     And I visit dashboard page
 
   Scenario: I should see projects list
+    Then I should see "New Project" link
     Then I should see "Shop" project link
     Then I should see "Shop" project CI status
 
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 5c14c5db665d00d0bd029a35a8d2d07415bb86ba..0d6f73501817cfae0a1264382bf2493d2b050bb1 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -7,8 +7,9 @@ Feature: Project Active Tab
 
   Scenario: On Project Home
     Given I visit my project's home page
-    Then the active main tab should be Home
-    And no other main tabs should be active
+    Then the active sub tab should be Home
+    And no other sub tabs should be active
+    And the active main tab should be Project
 
   Scenario: On Project Repository
     Given I visit my project's files page
@@ -34,36 +35,45 @@ Feature: Project Active Tab
 
   Scenario: On Project Home/Show
     Given I visit my project's home page
-    Then the active main tab should be Home
+    Then the active sub tab should be Home
+    And no other sub tabs should be active
+    And the active main tab should be Project
     And no other main tabs should be active
 
+  Scenario: On Project Home/Activity
+    Given I visit my project's home page
+    And I click the "Activity" tab
+    Then the active sub tab should be Activity
+    And no other sub tabs should be active
+    And the active main tab should be Project
+
   # Sub Tabs: Settings
 
   Scenario: On Project Settings/Integrations
     Given I visit my project's settings page
     And I click the "Integrations" tab
-    Then the active sub nav should be Integrations
-    And no other sub navs should be active
+    Then the active sub tab should be Integrations
+    And no other sub tabs should be active
     And the active main tab should be Settings
 
-  Scenario: On Project Settings/Deploy Keys
+  Scenario: On Project Settings/Repository
     Given I visit my project's settings page
-    And I click the "Deploy Keys" tab
-    Then the active sub nav should be Deploy Keys
-    And no other sub navs should be active
+    And I click the "Repository" tab
+    Then the active sub tab should be Repository
+    And no other sub tabs should be active
     And the active main tab should be Settings
 
   Scenario: On Project Settings/Pages
     Given I visit my project's settings page
     And I click the "Pages" tab
-    Then the active sub nav should be Pages
-    And no other sub navs should be active
+    Then the active sub tab should be Pages
+    And no other sub tabs should be active
     And the active main tab should be Settings
 
   Scenario: On Project Members
     Given I visit my project's members page
-    Then the active sub nav should be Members
-    And no other sub navs should be active
+    Then the active sub tab should be Members
+    And no other sub tabs should be active
     And the active main tab should be Settings
 
   # Sub Tabs: Repository
@@ -80,9 +90,9 @@ Feature: Project Active Tab
     And no other sub tabs should be active
     And the active main tab should be Repository
 
-  Scenario: On Project Repository/Network
-    Given I visit my project's network page
-    Then the active sub tab should be Network
+  Scenario: On Project Repository/Graph
+    Given I visit my project's graph page
+    Then the active sub tab should be Graph
     And no other sub tabs should be active
     And the active main tab should be Repository
 
@@ -93,6 +103,13 @@ Feature: Project Active Tab
     And no other sub tabs should be active
     And the active main tab should be Repository
 
+  Scenario: On Project Repository/Charts
+    Given I visit my project's commits page
+    And I click the "Charts" tab
+    Then the active sub tab should be Charts
+    And no other sub tabs should be active
+    And the active main tab should be Repository
+
   Scenario: On Project Repository/Branches
     Given I visit my project's commits page
     And I click the "Branches" tab
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 88fef674c0cf7c67450a36335556b130f3f70f6e..c57376aecffb8515a3f4dc7f0642e0e3dbff5096 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -13,6 +13,7 @@ Feature: Project Commits Branches
     Given I visit project protected branches page
     Then I should see "Shop" protected branches list
 
+  @javascript
   Scenario: I create a branch
     Given I visit project branches page
     And I click new branch link
@@ -33,12 +34,7 @@ Feature: Project Commits Branches
     And I submit new branch form with invalid name
     Then I should see new an error that branch is invalid
 
-  Scenario: I create a branch with invalid reference
-    Given I visit project branches page
-    And I click new branch link
-    And I submit new branch form with invalid reference
-    Then I should see new an error that ref is invalid
-
+  @javascript
   Scenario: I create a branch that already exists
     Given I visit project branches page
     And I click new branch link
diff --git a/features/project/graph.feature b/features/project/graph.feature
index 63793d6f989190409987d206b25e7ccb66d679ab..b25c73ad87084046420f141e8daa543a905271cf 100644
--- a/features/project/graph.feature
+++ b/features/project/graph.feature
@@ -9,9 +9,10 @@ Feature: Project Graph
     Then page should have graphs
 
   @javascript
-  Scenario: I should see project commits graphs
+  Scenario: I should see project languages & commits graphs on commits graph url
     When I visit project "Shop" commits graph page
     Then page should have commits graphs
+    Then page should have languages graphs
 
   @javascript
   Scenario: I should see project ci graphs
@@ -20,6 +21,13 @@ Feature: Project Graph
     Then page should have CI graphs
 
   @javascript
-  Scenario: I should see project languages graphs
+  Scenario: I should see project languages & commits graphs on language graph url
     When I visit project "Shop" languages graph page
     Then page should have languages graphs
+    Then page should have commits graphs
+
+  @javascript
+  Scenario: I should see project languages & commits graphs on charts url
+    When I visit project "Shop" chart page
+    Then page should have languages graphs
+    Then page should have commits graphs
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
index f0fd414a9f913fe50a5b463a4369932d7bed73cb..1d7adfdd2c2f9f667272846dba26acdc4ec3c367 100644
--- a/features/project/issues/award_emoji.feature
+++ b/features/project/issues/award_emoji.feature
@@ -42,4 +42,4 @@ Feature: Award Emoji
   @javascript
   Scenario: I add award emoji using regular comment
     Given I leave comment with a single emoji
-    Then I have award added
+    Then I have new comment with emoji added
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index f71f69ef060d468d0c8d0fb4afa7a2aab1526e79..b47fca31ef2070eccd2a877d2dfcb14c1cf3f448 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -19,15 +19,16 @@ Feature: Project Shortcuts
     Then the active sub tab should be Commits
 
   @javascript
-  Scenario: Navigate to network tab
+  Scenario: Navigate to graph tab
     Given I press "g" and "n"
-    Then the active sub tab should be Network
+    Then the active sub tab should be Graph
     And the active main tab should be Repository
 
   @javascript
-  Scenario: Navigate to graphs tab
+  Scenario: Navigate to repository charts tab
     Given I press "g" and "g"
-    Then the active main tab should be Graphs
+    Then the active sub tab should be Charts
+    And the active main tab should be Repository
 
   @javascript
   Scenario: Navigate to issues tab
@@ -52,9 +53,11 @@ Feature: Project Shortcuts
   @javascript
   Scenario: Navigate to project home
     Given I press "g" and "p"
-    Then the active main tab should be Home
+    Then the active sub tab should be Home
+    And the active main tab should be Project
 
   @javascript
   Scenario: Navigate to project feed
     Given I press "g" and "e"
-    Then the active main tab should be Activity
+    Then the active sub tab should be Activity
+    And the active main tab should be Project
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index eb906a55a831a20125797e06fa5ec0f89c4aedd6..9f01dff776fa2455d1f5cdd4bb4055b3535f6ef9 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
   end
 
   def should_not_see_todo(title)
-    expect(page).not_to have_content title
+    expect(page).not_to have_visible_content title
+  end
+
+  def have_visible_content(text)
+    have_css('*', text: text, visible: true)
   end
 
   def john_doe
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 2b4a5ab08642fe7d4f212148e423230a9c8f6996..7dc33ab5683469249b167f4340e1f07b563b3016 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
 
   step 'I should see an http link to the repository' do
     project = Project.find_by(name: 'Community')
-    expect(page).to have_field('project_clone', with: project.http_url_to_repo)
+    expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
   end
 
   step 'I should see an ssh link to the repository' do
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 70e23098dde08e68c525e117ac2fd4fe7ee1e673..20204ad8654bebc247011352032610b80450738a 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
   include SharedUser
 
   step 'I click on group milestones' do
-    page.within('.layout-nav') do
-      click_link 'Milestones'
-    end
+    visit group_milestones_path('owned')
   end
 
   step 'I should see group milestones index page has no milestones' do
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index e842d7bec2b93853664e8ed249317bcf206ee5cb..4befd49ac81d2c124eb96ec1d25fc5c5ba76f34e 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -22,37 +22,53 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
   end
 
   step 'I click the "Edit Project"' do
-    page.within '.layout-nav .controls' do
+    page.within '.sub-nav' do
       click_link('Edit Project')
     end
   end
 
   step 'I click the "Integrations" tab' do
-    click_link('Integrations')
+    page.within '.sub-nav' do
+      click_link('Integrations')
+    end
   end
 
-  step 'I click the "Deploy Keys" tab' do
-    click_link('Deploy Keys')
+  step 'I click the "Repository" tab' do
+    page.within '.sub-nav' do
+      click_link('Repository')
+    end
   end
 
   step 'I click the "Pages" tab' do
-    click_link('Pages')
+    page.within '.sub-nav' do
+      click_link('Pages')
+    end
   end
 
-  step 'the active sub nav should be Members' do
-    ensure_active_sub_nav('Members')
+  step 'I click the "Activity" tab' do
+    page.within '.sub-nav' do
+      click_link('Activity')
+    end
   end
 
-  step 'the active sub nav should be Integrations' do
-    ensure_active_sub_nav('Integrations')
+  step 'the active sub tab should be Members' do
+    ensure_active_sub_tab('Members')
   end
 
-  step 'the active sub nav should be Deploy Keys' do
-    ensure_active_sub_nav('Deploy Keys')
+  step 'the active sub tab should be Integrations' do
+    ensure_active_sub_tab('Integrations')
   end
 
-  step 'the active sub nav should be Pages' do
-    ensure_active_sub_nav('Pages')
+  step 'the active sub tab should be Repository' do
+    ensure_active_sub_tab('Repository')
+  end
+
+  step 'the active sub tab should be Pages' do
+    ensure_active_sub_tab('Pages')
+  end
+
+  step 'the active sub tab should be Activity' do
+    ensure_active_sub_tab('Activity')
   end
 
   # Sub Tabs: Commits
@@ -71,6 +87,12 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
     click_link('Tags')
   end
 
+  step 'I click the "Charts" tab' do
+    page.within '.sub-nav' do
+      click_link('Charts')
+    end
+  end
+
   step 'the active sub tab should be Compare' do
     ensure_active_sub_tab('Compare')
   end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 055fca036d3014c49d657deee90dcd1a38923cba..be0f6eee55a69279d45804dfd6dd30807165572e 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -76,7 +76,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
     base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
     params = JSON.parse(Base64.urlsafe_decode64(base64_params))
 
-    expect(params.keys).to eq(['Archive', 'Entry'])
+    expect(params.keys).to eq(%w(Archive Entry))
     expect(params['Archive']).to end_with('build_artifacts.zip')
     expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
   end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 5f9b9e0445e85970786f10a770eb363eeb5543d7..ccaf3237815eb0c85b95db0e32afbf4100ada426 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
 
   step 'I submit new branch form' do
     fill_in 'branch_name', with: 'deploy_keys'
-    fill_in 'ref', with: 'master'
+    select_branch('master')
     click_button 'Create branch'
   end
 
   step 'I submit new branch form with invalid name' do
     fill_in 'branch_name', with: '1.0 stable'
-    fill_in 'ref', with: 'master'
-    click_button 'Create branch'
-  end
-
-  step 'I submit new branch form with invalid reference' do
-    fill_in 'branch_name', with: 'foo'
-    fill_in 'ref', with: 'foo'
+    select_branch('master')
     click_button 'Create branch'
   end
 
   step 'I submit new branch form with branch that already exists' do
     fill_in 'branch_name', with: 'master'
-    fill_in 'ref', with: 'master'
+    select_branch('master')
     click_button 'Create branch'
   end
 
@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
     expect(page).to have_content "can't contain spaces"
   end
 
-  step 'I should see new an error that ref is invalid' do
-    expect(page).to have_content 'Invalid reference name'
-  end
-
   step 'I should see new an error that branch already exists' do
     expect(page).to have_content 'Branch already exists'
   end
@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
   step "I should not see branch 'improve/awesome'" do
     expect(page.all(visible: true)).not_to have_content 'improve/awesome'
   end
+
+  def select_branch(branch_name)
+    click_button 'master'
+
+    page.within '#new-branch-form .dropdown-menu' do
+      click_link branch_name
+    end
+  end
 end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 18e267294e408939dd2f037520f67e3ad75c88b5..cf75fac8ac6e05865767cec8689c9995a2aea912 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
   end
 
   step 'I see commit ci info' do
-    expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
+    expect(page).to have_content "Pipeline #1 pending"
   end
 
   step 'I search "submodules" commits' do
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
index 94a5d4e2e4de1ffb34a9fd60dec2a5bd232f2ad0..c97464073446c0dacf6c8d547ce2a64dddc56740 100644
--- a/features/steps/project/commits/revert.rb
+++ b/features/steps/project/commits/revert.rb
@@ -36,5 +36,6 @@ class Spinach::Features::RevertCommits < Spinach::FeatureSteps
 
   step 'I should see the new merge request notice' do
     page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+    page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
   end
 end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index edf78f62f9a213f21b10ce8abb7243150ea1c6b9..580a19494c2d90d60668726559b466de78971ffd 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
   end
 
   step 'I should be on deploy keys page' do
-    expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project)
+    expect(current_path).to eq namespace_project_settings_repository_path(@project.namespace, @project)
   end
 
   step 'I should see newly created deploy key' do
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 9a6c04fba7ad4740ee718b1d42686df861da4d13..79db9728227699925df29b7265b93a072591dc96 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -56,7 +56,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
   end
 
   step 'I should see my fork on the list' do
-    page.within('.projects-list-holder') do
+    page.within('.js-projects-list-holder') do
       project = @user.fork_of(@project)
       expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
     end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 48ac7a98f0dd22e694c9f368ca5eada394af12bf..176d04d721cc430a92d279b108780ada5630f995 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -18,6 +18,10 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
     visit languages_namespace_project_graph_path(project.namespace, project, "master")
   end
 
+  step 'I visit project "Shop" chart page' do
+    visit charts_namespace_project_graph_path(project.namespace, project, "master")
+  end
+
   step 'page should have languages graphs' do
     expect(page).to have_content /Ruby 66.* %/
     expect(page).to have_content /JavaScript 22.* %/
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index cbe5738e7e4382cedf2c98789f05757c4bdcc796..1762d5bdf95b942734fc2baf05d5516f535aac42 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -44,6 +44,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
     end
   end
 
+  step 'I have new comment with emoji added' do
+    expect(page).to have_selector ".emoji[title=':smile:']"
+  end
+
   step 'I have award added' do
     page.within '.awards' do
       expect(page).to have_selector '.js-emoji-btn'
@@ -86,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
 
   step 'I see search result for "hand"' do
     page.within '.emoji-menu-content' do
-      expect(page).to have_selector '[data-emoji="raised_hand"]'
+      expect(page).to have_selector '[data-name="raised_hand"]'
     end
   end
 
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index ff9251615c930d84c146e83e15cf879c132bcf53..370e46265c7b0d121d2cc155bc8576b2807074dd 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -66,7 +66,7 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
   end
 
   step 'page should have "v1.0.0" in title' do
-    expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
+    expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false
   end
 
   step 'page should only have content from "v1.0.0"' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
index c80c6273807f65cf95c7ba5bc05bfa9393eaa491..4045955a8b9e04ac88ea61e973fb17dd837ce3d8 100644
--- a/features/steps/project/pages.rb
+++ b/features/steps/project/pages.rb
@@ -53,13 +53,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
   end
 
   step 'pages are exposed on external HTTP address' do
-    allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+    allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
     allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
   end
 
   step 'pages are exposed on external HTTPS address' do
-    allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
-    allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+    allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
+    allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
   end
 
   step 'I should be able to add a New Domain' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce1e2cf755584fba4f2350f2d53c419..5c47eaf0279dab31e87d6f329f310c3bba9ed2f5 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -48,7 +48,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I click link "Raw"' do
-    click_link 'Raw'
+    click_link 'Open raw'
   end
 
   step 'I should see raw file content' do
@@ -82,7 +82,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I fill the new branch name' do
-    fill_in :target_branch, with: 'new_branch_name', visible: true
+    first('button.js-target-branch', visible: true).click
+    first('.create-new-branch', visible: true).click
+    first('#new_branch_name', visible: true).set('new_branch_name')
+    first('.js-new-branch-btn', visible: true).click
   end
 
   step 'I fill the new file name with an illegal name' do
@@ -334,6 +337,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I click on "files/lfs/lfs_object.iso" file in repo' do
+    allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
     visit namespace_project_tree_path(@project.namespace, @project, "lfs")
     click_link 'files'
     click_link "lfs"
@@ -352,7 +356,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
 
   step 'I should see buttons for allowed commands' do
     page.within '.content' do
-      expect(page).to have_content 'Raw'
+      expect(page).to have_link 'Open raw'
       expect(page).to have_content 'History'
       expect(page).to have_content 'Permalink'
       expect(page).not_to have_content 'Edit'
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 79dde620265d7a2f51145f20a1b44731df13a9a3..3d9cedf5c2d649b5e7b2a1c2fceeb327fe780241 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -153,7 +153,7 @@ module SharedIssuable
 
     case type
     when :issue
-      attrs.merge!(project: project)
+      attrs[:project] = project
     when :merge_request
       attrs.merge!(
         source_project: project,
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 718cf9247293a66c2297c9ed0db6f3006911a0a3..d5b3bb34d7a128cc633ec6e84d1933e336d18fd3 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -232,7 +232,7 @@ module SharedPaths
     visit stats_namespace_project_repository_path(@project.namespace, @project)
   end
 
-  step "I visit my project's network page" do
+  step "I visit my project's graph page" do
     # Stub Graph max_size to speed up test (10 commits vs. 650)
     Network::Graph.stub(max_count: 10)
 
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index dae248b8b7e751316904e140e047dd8a8f932ced..345a28f27dc1b14b6ae09e24332ef680626f72c7 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -166,11 +166,15 @@ module SharedProject
   end
 
   step 'I should see project "Internal"' do
-    expect(page).to have_content "Internal"
+    page.within '.js-projects-list-holder' do
+      expect(page).to have_content "Internal"
+    end
   end
 
   step 'I should not see project "Internal"' do
-    expect(page).not_to have_content "Internal"
+    page.within '.js-projects-list-holder' do
+      expect(page).not_to have_content "Internal"
+    end
   end
 
   step 'public project "Community"' do
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index d6024212601f060340dd267f697e28adec52d275..0cb9229dbaeb6aab216f9b5c5de74ec7415f2af7 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -4,7 +4,7 @@ module SharedProjectTab
   include Spinach::DSL
   include SharedActiveTab
 
-  step 'the active main tab should be Home' do
+  step 'the active main tab should be Project' do
     ensure_active_main_tab('Project')
   end
 
@@ -12,16 +12,12 @@ module SharedProjectTab
     ensure_active_main_tab('Repository')
   end
 
-  step 'the active main tab should be Graphs' do
-    ensure_active_main_tab('Graphs')
-  end
-
   step 'the active main tab should be Issues' do
     ensure_active_main_tab('Issues')
   end
 
-  step 'the active main tab should be Members' do
-    ensure_active_main_tab('Members')
+  step 'the active sub tab should be Members' do
+    ensure_active_sub_tab('Members')
   end
 
   step 'the active main tab should be Merge Requests' do
@@ -37,15 +33,11 @@ module SharedProjectTab
   end
 
   step 'the active main tab should be Settings' do
-    expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0)
+    ensure_active_main_tab('Settings')
   end
 
-  step 'the active main tab should be Activity' do
-    ensure_active_main_tab('Activity')
-  end
-
-  step 'the active sub tab should be Network' do
-    ensure_active_sub_tab('Network')
+  step 'the active sub tab should be Graph' do
+    ensure_active_sub_tab('Graph')
   end
 
   step 'the active sub tab should be Files' do
@@ -55,4 +47,16 @@ module SharedProjectTab
   step 'the active sub tab should be Commits' do
     ensure_active_sub_tab('Commits')
   end
+
+  step 'the active sub tab should be Home' do
+    ensure_active_sub_tab('Home')
+  end
+
+  step 'the active sub tab should be Activity' do
+    ensure_active_sub_tab('Activity')
+  end
+
+  step 'the active sub tab should be Charts' do
+    ensure_active_sub_tab('Charts')
+  end
 end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 47372df152d59a77e6c900100ff7afbe26cdff9a..c0c489d277508cc4b7af81166583b77e0a747ff0 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,8 +1,9 @@
 require 'spinach/capybara'
 require 'capybara/poltergeist'
+require 'capybara-screenshot/spinach'
 
 # Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
 
 Capybara.javascript_driver = :poltergeist
 Capybara.register_driver :poltergeist do |app|
@@ -20,12 +21,8 @@ end
 Capybara.default_max_wait_time = timeout
 Capybara.ignore_hidden_elements = false
 
-unless ENV['CI'] || ENV['CI_SERVER']
-  require 'capybara-screenshot/spinach'
-
-  # Keep only the screenshots generated from the last failing test suite
-  Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
 
 Spinach.hooks.before_run do
   TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 078d3413f338eaefd3c001e84509ddb8bf3123ff..3cbc4702daca2622318fb9e8eb10644aebe1ccc9 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -1,11622 +1,10748 @@
-[
-  {
-    "name": "100",
-    "unicode": "1F4AF",
+{
+  "100": {
+    "category": "symbols",
+    "moji": "💯",
+    "unicodeVersion": "6.0",
     "digest": "add3bd7d06b6dd445788b277f8c9e5dcf42a54d3ec8b7fb9e7a39695dd95d094"
   },
-  {
-    "name": "1234",
-    "unicode": "1F522",
+  "1234": {
+    "category": "symbols",
+    "moji": "🔢",
+    "unicodeVersion": "6.0",
     "digest": "c5ac5c8147f5bfd644fad6b470432bba86ffc7bcee04a0e0d277cd1ca485207f"
   },
-  {
-    "name": "8ball",
-    "unicode": "1F3B1",
+  "8ball": {
+    "category": "activity",
+    "moji": "🎱",
+    "unicodeVersion": "6.0",
     "digest": "a6e6855775b66c505adee65926a264103ebddf2e2d963db7c009b4fec3a24178"
   },
-  {
-    "name": "a",
-    "unicode": "1F170",
+  "a": {
+    "category": "symbols",
+    "moji": "🅰",
+    "unicodeVersion": "6.0",
     "digest": "bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc"
   },
-  {
-    "name": "ab",
-    "unicode": "1F18E",
+  "ab": {
+    "category": "symbols",
+    "moji": "🆎",
+    "unicodeVersion": "6.0",
     "digest": "67430fe5fce981160e2ea9052962e49f264322d3abfc2828fbc311b6cdf67ae8"
   },
-  {
-    "name": "abc",
-    "unicode": "1F524",
+  "abc": {
+    "category": "symbols",
+    "moji": "🔤",
+    "unicodeVersion": "6.0",
     "digest": "282c817ee3414d77a74b815962c33dd9fe71fabaea8c7a9cec466100fbe32187"
   },
-  {
-    "name": "abcd",
-    "unicode": "1F521",
+  "abcd": {
+    "category": "symbols",
+    "moji": "🔡",
+    "unicodeVersion": "6.0",
     "digest": "686728c759f4683c64762ee4eda0a91bf2041f0ae4f358aacf6c09bf51892eff"
   },
-  {
-    "name": "accept",
-    "unicode": "1F251",
+  "accept": {
+    "category": "symbols",
+    "moji": "🉑",
+    "unicodeVersion": "6.0",
     "digest": "7208d34c761f10a7fd28f98e25535eba13ff91a64442fc282a98bb77722614f1"
   },
-  {
-    "name": "aerial_tramway",
-    "unicode": "1F6A1",
+  "aerial_tramway": {
+    "category": "travel",
+    "moji": "🚡",
+    "unicodeVersion": "6.0",
     "digest": "98df666f34370fc34ce280d84bba5a7e617f733fbbfe66caa424b2afa6ab6777"
   },
-  {
-    "name": "airplane",
-    "unicode": "2708",
+  "airplane": {
+    "category": "travel",
+    "moji": "✈",
+    "unicodeVersion": "1.1",
     "digest": "cc12cf259ef88e57717620cd2bd5aa6a02a8631ee532a3bde24bee78edc5de33"
   },
-  {
-    "name": "airplane_arriving",
-    "unicode": "1F6EC",
+  "airplane_arriving": {
+    "category": "travel",
+    "moji": "🛬",
+    "unicodeVersion": "7.0",
     "digest": "80d5b4675f91c4cff06d146d795a065b0ce2a74557df4d9e3314e3d3b5c4ae82"
   },
-  {
-    "name": "airplane_departure",
-    "unicode": "1F6EB",
+  "airplane_departure": {
+    "category": "travel",
+    "moji": "🛫",
+    "unicodeVersion": "7.0",
     "digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332"
   },
-  {
-    "name": "airplane_small",
-    "unicode": "1F6E9",
+  "airplane_small": {
+    "category": "travel",
+    "moji": "🛩",
+    "unicodeVersion": "7.0",
     "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
   },
-  {
-    "name": "small_airplane",
-    "unicode": "1F6E9",
-    "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
-  },
-  {
-    "name": "alarm_clock",
-    "unicode": "23F0",
+  "alarm_clock": {
+    "category": "objects",
+    "moji": "⏰",
+    "unicodeVersion": "6.0",
     "digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599"
   },
-  {
-    "name": "alembic",
-    "unicode": "2697",
+  "alembic": {
+    "category": "objects",
+    "moji": "⚗",
+    "unicodeVersion": "4.1",
     "digest": "c94b2a4bf24ccf4db27a22c9725cfe900f4a99ec49ef2411d67952bcb2ca1bfb"
   },
-  {
-    "name": "alien",
-    "unicode": "1F47D",
+  "alien": {
+    "category": "people",
+    "moji": "👽",
+    "unicodeVersion": "6.0",
     "digest": "856ba98202b244c13a5ee3014a6f7ad592d8c119a30d79e4fc790b74b0e321f7"
   },
-  {
-    "name": "ambulance",
-    "unicode": "1F691",
+  "ambulance": {
+    "category": "travel",
+    "moji": "🚑",
+    "unicodeVersion": "6.0",
     "digest": "d9b3c1873de496a4554e715342c72290fb69a9c6766d7885f38bfe9491d052da"
   },
-  {
-    "name": "amphora",
-    "unicode": "1F3FA",
+  "amphora": {
+    "category": "objects",
+    "moji": "🏺",
+    "unicodeVersion": "8.0",
     "digest": "4015f907b649b5e348502cc0e3685ed184e180dca5cc81c43ec516e14df127bf"
   },
-  {
-    "name": "anchor",
-    "unicode": "2693",
+  "anchor": {
+    "category": "travel",
+    "moji": "⚓",
+    "unicodeVersion": "4.1",
     "digest": "2b29b34ef896ebab70016301e3d1880209bbc3c5a5b8d832e43afff9b17ad792"
   },
-  {
-    "name": "angel",
-    "unicode": "1F47C",
+  "angel": {
+    "category": "people",
+    "moji": "👼",
+    "unicodeVersion": "6.0",
     "digest": "db75c2460aaf9cd07cb41fe22c8a6079f3667ffe612a71611358720e2b5512a4"
   },
-  {
-    "name": "angel_tone1",
-    "unicode": "1F47C-1F3FB",
+  "angel_tone1": {
+    "category": "people",
+    "moji": "👼🏻",
+    "unicodeVersion": "8.0",
     "digest": "5871a622469b96296365adaf77d83167759692124c20e5a6e062a525af33472a"
   },
-  {
-    "name": "angel_tone2",
-    "unicode": "1F47C-1F3FC",
+  "angel_tone2": {
+    "category": "people",
+    "moji": "👼🏼",
+    "unicodeVersion": "8.0",
     "digest": "f5993198a5d9daf39e761c783461f07bca237f4e9b739ac300bb8ca001a69a1a"
   },
-  {
-    "name": "angel_tone3",
-    "unicode": "1F47C-1F3FD",
+  "angel_tone3": {
+    "category": "people",
+    "moji": "👼🏽",
+    "unicodeVersion": "8.0",
     "digest": "f0c97a7c4354626267d6ab0f388e4297ad255ab9b061f9c68fbcaa0abfc52783"
   },
-  {
-    "name": "angel_tone4",
-    "unicode": "1F47C-1F3FE",
+  "angel_tone4": {
+    "category": "people",
+    "moji": "👼🏾",
+    "unicodeVersion": "8.0",
     "digest": "6e5dc724c1939d1b0d1a91343662b5bd61ced7709c97802977145ffab6a1f7ac"
   },
-  {
-    "name": "angel_tone5",
-    "unicode": "1F47C-1F3FF",
+  "angel_tone5": {
+    "category": "people",
+    "moji": "👼🏿",
+    "unicodeVersion": "8.0",
     "digest": "52186e1de350c27d25d6010edf44f64a30338b65912ca178429fbcfbd88113c2"
   },
-  {
-    "name": "anger",
-    "unicode": "1F4A2",
+  "anger": {
+    "category": "symbols",
+    "moji": "💢",
+    "unicodeVersion": "6.0",
     "digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f"
   },
-  {
-    "name": "anger_right",
-    "unicode": "1F5EF",
-    "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
-  },
-  {
-    "name": "right_anger_bubble",
-    "unicode": "1F5EF",
+  "anger_right": {
+    "category": "symbols",
+    "moji": "🗯",
+    "unicodeVersion": "7.0",
     "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
   },
-  {
-    "name": "angry",
-    "unicode": "1F620",
+  "angry": {
+    "category": "people",
+    "moji": "😠",
+    "unicodeVersion": "6.0",
     "digest": "7e09e7e821f511606341fb5ce4011a8ed9809766ab86b7983ffa6ea352b39ec1"
   },
-  {
-    "name": "anguished",
-    "unicode": "1F627",
-    "digest": "a2b6f052996969a17150249d9ef5db742da3d6585bd38ca61eb14c4c13cda54f"
-  },
-  {
-    "name": "ant",
-    "unicode": "1F41C",
+  "ant": {
+    "category": "nature",
+    "moji": "🐜",
+    "unicodeVersion": "6.0",
     "digest": "929abeaff7ba21ab71cd1ab798af7a6b611e3b3ce1af80cede09a116b223e442"
   },
-  {
-    "name": "apple",
-    "unicode": "1F34E",
+  "apple": {
+    "category": "food",
+    "moji": "🍎",
+    "unicodeVersion": "6.0",
     "digest": "2a1b85ce57e3d236ae7777dcf332ec37d03bfd7b19806521a353bc532083224d"
   },
-  {
-    "name": "aquarius",
-    "unicode": "2652",
+  "aquarius": {
+    "category": "symbols",
+    "moji": "♒",
+    "unicodeVersion": "1.1",
     "digest": "fdc42cd41b0dace5eae6baba3143f1e40295d48a29e7103a5bba1d84a056c39d"
   },
-  {
-    "name": "aries",
-    "unicode": "2648",
+  "aries": {
+    "category": "symbols",
+    "moji": "♈",
+    "unicodeVersion": "1.1",
     "digest": "deb135debcde0a98f40361a84ab64d57c18b5b445cd2f4199e8936f052899737"
   },
-  {
-    "name": "arrow_backward",
-    "unicode": "25C0",
+  "arrow_backward": {
+    "category": "symbols",
+    "moji": "◀",
+    "unicodeVersion": "1.1",
     "digest": "e162ac82e90d1e925d479fa5c45b9340e0a53287be04e43cbbb2a89c7e7e45e4"
   },
-  {
-    "name": "arrow_double_down",
-    "unicode": "23EC",
+  "arrow_double_down": {
+    "category": "symbols",
+    "moji": "⏬",
+    "unicodeVersion": "6.0",
     "digest": "03ca890b05338d40972c7a056d672df620a203c6ca52ff3ff530f1a710905507"
   },
-  {
-    "name": "arrow_double_up",
-    "unicode": "23EB",
+  "arrow_double_up": {
+    "category": "symbols",
+    "moji": "⏫",
+    "unicodeVersion": "6.0",
     "digest": "e753f05bce993d62d5dc79e33c441ced059381b6ce21fa3ea4200f1b3236e59d"
   },
-  {
-    "name": "arrow_down",
-    "unicode": "2B07",
+  "arrow_down": {
+    "category": "symbols",
+    "moji": "⬇",
+    "unicodeVersion": "4.0",
     "digest": "9bf1bd2ea652ca9321087de58c7a112ea04c35676a6ee0766154183f8b95af6c"
   },
-  {
-    "name": "arrow_down_small",
-    "unicode": "1F53D",
+  "arrow_down_small": {
+    "category": "symbols",
+    "moji": "🔽",
+    "unicodeVersion": "6.0",
     "digest": "7766198bc60cf59d6cdaeeaa700c2282bfff2f0fdeb22cf4581ca284b87a3bb7"
   },
-  {
-    "name": "arrow_forward",
-    "unicode": "25B6",
+  "arrow_forward": {
+    "category": "symbols",
+    "moji": "▶",
+    "unicodeVersion": "1.1",
     "digest": "db77d9accd1e02224f5d612f79cd691e6befdf22063475204836be6572510fb7"
   },
-  {
-    "name": "arrow_heading_down",
-    "unicode": "2935",
+  "arrow_heading_down": {
+    "category": "symbols",
+    "moji": "⤵",
+    "unicodeVersion": "3.2",
     "digest": "f5396069c8f63c13e6c3e0ecd34267c932451309ade9c1171d410563153bf909"
   },
-  {
-    "name": "arrow_heading_up",
-    "unicode": "2934",
+  "arrow_heading_up": {
+    "category": "symbols",
+    "moji": "⤴",
+    "unicodeVersion": "3.2",
     "digest": "1cad71923fa3df24cf543cae4ce775b0f74936f2edd685fd86a7525c41a14568"
   },
-  {
-    "name": "arrow_left",
-    "unicode": "2B05",
+  "arrow_left": {
+    "category": "symbols",
+    "moji": "⬅",
+    "unicodeVersion": "4.0",
     "digest": "b629bb3dbe161ef89cfcfced0c7968a68e44a019ad509132987e4973bdc874e7"
   },
-  {
-    "name": "arrow_lower_left",
-    "unicode": "2199",
+  "arrow_lower_left": {
+    "category": "symbols",
+    "moji": "↙",
+    "unicodeVersion": "1.1",
     "digest": "879136ba0e24e6bf3be70118abcb716d71bd74f7b62347bc052b6533c0ea534d"
   },
-  {
-    "name": "arrow_lower_right",
-    "unicode": "2198",
+  "arrow_lower_right": {
+    "category": "symbols",
+    "moji": "↘",
+    "unicodeVersion": "1.1",
     "digest": "86d52ac9b961991e3aaa6a9f9b5ace4db6ffd1b5c171c09c23b516473b55066d"
   },
-  {
-    "name": "arrow_right",
-    "unicode": "27A1",
+  "arrow_right": {
+    "category": "symbols",
+    "moji": "➡",
+    "unicodeVersion": "1.1",
     "digest": "45f26a1cbb0f00ed3609b39da52e9d9e896a77e361c4c8036b1bf8038171bd49"
   },
-  {
-    "name": "arrow_right_hook",
-    "unicode": "21AA",
+  "arrow_right_hook": {
+    "category": "symbols",
+    "moji": "↪",
+    "unicodeVersion": "1.1",
     "digest": "4f452679c71bcea4fc4a701c55156fef3ddc1ebbc70570bedfc9d3a029637ab1"
   },
-  {
-    "name": "arrow_up",
-    "unicode": "2B06",
+  "arrow_up": {
+    "category": "symbols",
+    "moji": "⬆",
+    "unicodeVersion": "4.0",
     "digest": "982b988ef6651d8a71867ba7c87f640f62dd0eeb0b7c358f5a5c37e8fe507b8b"
   },
-  {
-    "name": "arrow_up_down",
-    "unicode": "2195",
+  "arrow_up_down": {
+    "category": "symbols",
+    "moji": "↕",
+    "unicodeVersion": "1.1",
     "digest": "645ed8fb6646f49bfd95af1752336deacdadbe5cba13904023a704288f3b0e2c"
   },
-  {
-    "name": "arrow_up_small",
-    "unicode": "1F53C",
+  "arrow_up_small": {
+    "category": "symbols",
+    "moji": "🔼",
+    "unicodeVersion": "6.0",
     "digest": "4a8c5789c13a852517e639e7a62c2d331464e6fb0358985aa97c1515e97b5e8b"
   },
-  {
-    "name": "arrow_upper_left",
-    "unicode": "2196",
+  "arrow_upper_left": {
+    "category": "symbols",
+    "moji": "↖",
+    "unicodeVersion": "1.1",
     "digest": "79026f828d6ceb7c55a9542770962ba6dcd08203995f6ceeb70333a12307d376"
   },
-  {
-    "name": "arrow_upper_right",
-    "unicode": "2197",
+  "arrow_upper_right": {
+    "category": "symbols",
+    "moji": "↗",
+    "unicodeVersion": "1.1",
     "digest": "7e0f33dfbe65628991c170130d366a3e2cedaf8862ddfcaf3960f395d3da1926"
   },
-  {
-    "name": "arrows_clockwise",
-    "unicode": "1F503",
+  "arrows_clockwise": {
+    "category": "symbols",
+    "moji": "🔃",
+    "unicodeVersion": "6.0",
     "digest": "88669679977f7157f0acaa9d6a1b77ccf84d25eb78c5bc8afcde38d3635e7144"
   },
-  {
-    "name": "arrows_counterclockwise",
-    "unicode": "1F504",
+  "arrows_counterclockwise": {
+    "category": "symbols",
+    "moji": "🔄",
+    "unicodeVersion": "6.0",
     "digest": "a2c6a6d3643c128aee3304cd03bb3d7cfe4d35d3ba825bc9c1142d7832b4426e"
   },
-  {
-    "name": "art",
-    "unicode": "1F3A8",
+  "art": {
+    "category": "activity",
+    "moji": "🎨",
+    "unicodeVersion": "6.0",
     "digest": "b6bc6c4bfb594aadcbb641d006031867678504764bbe0ab84e7b08567a9498da"
   },
-  {
-    "name": "articulated_lorry",
-    "unicode": "1F69B",
+  "articulated_lorry": {
+    "category": "travel",
+    "moji": "🚛",
+    "unicodeVersion": "6.0",
     "digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa"
   },
-  {
-    "name": "asterisk",
-    "unicode": "002A-20E3",
-    "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
-  },
-  {
-    "name": "keycap_asterisk",
-    "unicode": "002A-20E3",
+  "asterisk": {
+    "category": "symbols",
+    "moji": "*⃣",
+    "unicodeVersion": "3.0",
     "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
   },
-  {
-    "name": "astonished",
-    "unicode": "1F632",
+  "astonished": {
+    "category": "people",
+    "moji": "😲",
+    "unicodeVersion": "6.0",
     "digest": "f8531bdda5070d10492709085f4ff652b8be9be6458758940358b9fc594a1f14"
   },
-  {
-    "name": "athletic_shoe",
-    "unicode": "1F45F",
+  "athletic_shoe": {
+    "category": "people",
+    "moji": "👟",
+    "unicodeVersion": "6.0",
     "digest": "1f90dc390e0dea679085465b7f9e786dfd7dd56a3b219987144ed37ab1e9bf95"
   },
-  {
-    "name": "atm",
-    "unicode": "1F3E7",
+  "atm": {
+    "category": "symbols",
+    "moji": "🏧",
+    "unicodeVersion": "6.0",
     "digest": "7d3ce6a6afb4951546883404b8e36904179f88f1aa533706cf7bf0bbe0d6fd3c"
   },
-  {
-    "name": "atom",
-    "unicode": "269B",
-    "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
-  },
-  {
-    "name": "atom_symbol",
-    "unicode": "269B",
+  "atom": {
+    "category": "symbols",
+    "moji": "⚛",
+    "unicodeVersion": "4.1",
     "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
   },
-  {
-    "name": "avocado",
-    "unicode": "1F951",
+  "avocado": {
+    "category": "food",
+    "moji": "🥑",
+    "unicodeVersion": "9.0",
     "digest": "bc1fb203d63b18985598400925de24050bb192afda1cbf0813f85cb139869eff"
   },
-  {
-    "name": "b",
-    "unicode": "1F171",
+  "b": {
+    "category": "symbols",
+    "moji": "🅱",
+    "unicodeVersion": "6.0",
     "digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf"
   },
-  {
-    "name": "baby",
-    "unicode": "1F476",
+  "baby": {
+    "category": "people",
+    "moji": "👶",
+    "unicodeVersion": "6.0",
     "digest": "219ae5a571aaf90c060956cd1c56dcc27708c827cecdca3ba1122058a3c4847b"
   },
-  {
-    "name": "baby_bottle",
-    "unicode": "1F37C",
+  "baby_bottle": {
+    "category": "food",
+    "moji": "🍼",
+    "unicodeVersion": "6.0",
     "digest": "4fb71689e9d634e8d1699cf454a71e43f2b5b1a5dbab0bf186626934fdf5b782"
   },
-  {
-    "name": "baby_chick",
-    "unicode": "1F424",
+  "baby_chick": {
+    "category": "nature",
+    "moji": "🐤",
+    "unicodeVersion": "6.0",
     "digest": "14119874e9b5548028dfb9cc593a541efc1d075ac839a565b92e0c3253cffe7e"
   },
-  {
-    "name": "baby_symbol",
-    "unicode": "1F6BC",
+  "baby_symbol": {
+    "category": "symbols",
+    "moji": "🚼",
+    "unicodeVersion": "6.0",
     "digest": "fb4db66868cda45ea3879ffc2ff4f763c56d2d889ae0ab17fe171129ede02f98"
   },
-  {
-    "name": "baby_tone1",
-    "unicode": "1F476-1F3FB",
+  "baby_tone1": {
+    "category": "people",
+    "moji": "👶🏻",
+    "unicodeVersion": "8.0",
     "digest": "cd3faf223a298c34e05d469d9d0db08438d97df7fd82c0973f8a9e07d553f5b1"
   },
-  {
-    "name": "baby_tone2",
-    "unicode": "1F476-1F3FC",
+  "baby_tone2": {
+    "category": "people",
+    "moji": "👶🏼",
+    "unicodeVersion": "8.0",
     "digest": "5b4539e22e0dd726c27eb8af2357f9240a52aed3f710f3234571cff029cc6198"
   },
-  {
-    "name": "baby_tone3",
-    "unicode": "1F476-1F3FD",
+  "baby_tone3": {
+    "category": "people",
+    "moji": "👶🏽",
+    "unicodeVersion": "8.0",
     "digest": "720e740e1ac63c6372269132b1fb6e07a6b91f5c808cc3adef59f0b4500e5e72"
   },
-  {
-    "name": "baby_tone4",
-    "unicode": "1F476-1F3FE",
+  "baby_tone4": {
+    "category": "people",
+    "moji": "👶🏾",
+    "unicodeVersion": "8.0",
     "digest": "5e43b69c509bd526ad6f081764578c30b6f3285fb7442222e05ccf62e53bfb64"
   },
-  {
-    "name": "baby_tone5",
-    "unicode": "1F476-1F3FF",
+  "baby_tone5": {
+    "category": "people",
+    "moji": "👶🏿",
+    "unicodeVersion": "8.0",
     "digest": "85bba6e0940ccfb99999fe124e815f9dd340d00a5568e13967b02245a62dbf54"
   },
-  {
-    "name": "back",
-    "unicode": "1F519",
+  "back": {
+    "category": "symbols",
+    "moji": "🔙",
+    "unicodeVersion": "6.0",
     "digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e"
   },
-  {
-    "name": "bacon",
-    "unicode": "1F953",
+  "bacon": {
+    "category": "food",
+    "moji": "🥓",
+    "unicodeVersion": "9.0",
     "digest": "18ad3817f1f88a69706db5727a58e763dde6c21a2d4f184c3d728c32dc5fa05a"
   },
-  {
-    "name": "badminton",
-    "unicode": "1F3F8",
+  "badminton": {
+    "category": "activity",
+    "moji": "🏸",
+    "unicodeVersion": "8.0",
     "digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66"
   },
-  {
-    "name": "baggage_claim",
-    "unicode": "1F6C4",
+  "baggage_claim": {
+    "category": "symbols",
+    "moji": "🛄",
+    "unicodeVersion": "6.0",
     "digest": "7d6bceca92c266da6d2b91dfcf244546fc11022e039e7da8e6888c1696bb2186"
   },
-  {
-    "name": "balloon",
-    "unicode": "1F388",
+  "balloon": {
+    "category": "objects",
+    "moji": "🎈",
+    "unicodeVersion": "6.0",
     "digest": "65760aedc1503b426927cff78c24449d563843a274961d962718fa9638375d54"
   },
-  {
-    "name": "ballot_box",
-    "unicode": "1F5F3",
-    "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
-  },
-  {
-    "name": "ballot_box_with_ballot",
-    "unicode": "1F5F3",
+  "ballot_box": {
+    "category": "objects",
+    "moji": "🗳",
+    "unicodeVersion": "7.0",
     "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
   },
-  {
-    "name": "ballot_box_with_check",
-    "unicode": "2611",
+  "ballot_box_with_check": {
+    "category": "symbols",
+    "moji": "☑",
+    "unicodeVersion": "1.1",
     "digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134"
   },
-  {
-    "name": "bamboo",
-    "unicode": "1F38D",
+  "bamboo": {
+    "category": "nature",
+    "moji": "🎍",
+    "unicodeVersion": "6.0",
     "digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd"
   },
-  {
-    "name": "banana",
-    "unicode": "1F34C",
+  "banana": {
+    "category": "food",
+    "moji": "🍌",
+    "unicodeVersion": "6.0",
     "digest": "f9e8ff910c282c20a8907ff64926b5de4ee250529a1ed718fb33302e6fff8dd9"
   },
-  {
-    "name": "bangbang",
-    "unicode": "203C",
+  "bangbang": {
+    "category": "symbols",
+    "moji": "‼",
+    "unicodeVersion": "1.1",
     "digest": "76536fee63fe964a3f3839d309b1f45028fb0c43f4d1eeee495f17e1532b4def"
   },
-  {
-    "name": "bank",
-    "unicode": "1F3E6",
+  "bank": {
+    "category": "travel",
+    "moji": "🏦",
+    "unicodeVersion": "6.0",
     "digest": "f5d2976bf6d521638ccacc74be06bd4abfeab06c5d898a9d245edad45a5b6306"
   },
-  {
-    "name": "bar_chart",
-    "unicode": "1F4CA",
+  "bar_chart": {
+    "category": "objects",
+    "moji": "📊",
+    "unicodeVersion": "6.0",
     "digest": "65a328a1b2d7a5332dd4d93f4dbca13d976f0a505b00835c3fc458e394804240"
   },
-  {
-    "name": "barber",
-    "unicode": "1F488",
+  "barber": {
+    "category": "objects",
+    "moji": "💈",
+    "unicodeVersion": "6.0",
     "digest": "5e8053d3bb3765a8632fd1cbfe21163f74ed79f6be377eb9603eaaf883d8dc46"
   },
-  {
-    "name": "baseball",
-    "unicode": "26BE",
+  "baseball": {
+    "category": "activity",
+    "moji": "⚾",
+    "unicodeVersion": "5.2",
     "digest": "46ac16f8b5455b942f6dbff9483a6fd277721e6719d2731573baabd21c44b34f"
   },
-  {
-    "name": "basketball",
-    "unicode": "1F3C0",
+  "basketball": {
+    "category": "activity",
+    "moji": "🏀",
+    "unicodeVersion": "6.0",
     "digest": "cc83e2aea8fcd2e9a5789e1932ee3766c40843c142fd3565c4e77dafb21ec7d7"
   },
-  {
-    "name": "basketball_player",
-    "unicode": "26F9",
-    "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
-  },
-  {
-    "name": "person_with_ball",
-    "unicode": "26F9",
+  "basketball_player": {
+    "category": "activity",
+    "moji": "⛹",
+    "unicodeVersion": "5.2",
     "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
   },
-  {
-    "name": "basketball_player_tone1",
-    "unicode": "26F9-1F3FB",
-    "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
-  },
-  {
-    "name": "person_with_ball_tone1",
-    "unicode": "26F9-1F3FB",
+  "basketball_player_tone1": {
+    "category": "activity",
+    "moji": "⛹🏻",
+    "unicodeVersion": "8.0",
     "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
   },
-  {
-    "name": "basketball_player_tone2",
-    "unicode": "26F9-1F3FC",
-    "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
-  },
-  {
-    "name": "person_with_ball_tone2",
-    "unicode": "26F9-1F3FC",
+  "basketball_player_tone2": {
+    "category": "activity",
+    "moji": "⛹🏼",
+    "unicodeVersion": "8.0",
     "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
   },
-  {
-    "name": "basketball_player_tone3",
-    "unicode": "26F9-1F3FD",
-    "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
-  },
-  {
-    "name": "person_with_ball_tone3",
-    "unicode": "26F9-1F3FD",
+  "basketball_player_tone3": {
+    "category": "activity",
+    "moji": "⛹🏽",
+    "unicodeVersion": "8.0",
     "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
   },
-  {
-    "name": "basketball_player_tone4",
-    "unicode": "26F9-1F3FE",
-    "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
-  },
-  {
-    "name": "person_with_ball_tone4",
-    "unicode": "26F9-1F3FE",
+  "basketball_player_tone4": {
+    "category": "activity",
+    "moji": "⛹🏾",
+    "unicodeVersion": "8.0",
     "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
   },
-  {
-    "name": "basketball_player_tone5",
-    "unicode": "26F9-1F3FF",
-    "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
-  },
-  {
-    "name": "person_with_ball_tone5",
-    "unicode": "26F9-1F3FF",
+  "basketball_player_tone5": {
+    "category": "activity",
+    "moji": "⛹🏿",
+    "unicodeVersion": "8.0",
     "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
   },
-  {
-    "name": "bat",
-    "unicode": "1F987",
+  "bat": {
+    "category": "nature",
+    "moji": "🦇",
+    "unicodeVersion": "9.0",
     "digest": "8fc19e0d7d6f80906bdbc06d616a810de66180d96cf28070a53fa61b88904535"
   },
-  {
-    "name": "bath",
-    "unicode": "1F6C0",
+  "bath": {
+    "category": "activity",
+    "moji": "🛀",
+    "unicodeVersion": "6.0",
     "digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917"
   },
-  {
-    "name": "bath_tone1",
-    "unicode": "1F6C0-1F3FB",
+  "bath_tone1": {
+    "category": "activity",
+    "moji": "🛀🏻",
+    "unicodeVersion": "8.0",
     "digest": "7ae2989e47788ba71359d52da68feec95aaff68a77d5a6556957df1617af8536"
   },
-  {
-    "name": "bath_tone2",
-    "unicode": "1F6C0-1F3FC",
+  "bath_tone2": {
+    "category": "activity",
+    "moji": "🛀🏼",
+    "unicodeVersion": "8.0",
     "digest": "2e86f8edad54d15a7094cd52160cbe51d10aa1750cfb0b3b58e93533f070e327"
   },
-  {
-    "name": "bath_tone3",
-    "unicode": "1F6C0-1F3FD",
+  "bath_tone3": {
+    "category": "activity",
+    "moji": "🛀🏽",
+    "unicodeVersion": "8.0",
     "digest": "654c0cd083a67ff330a38d07352876d265390e5399e5352598d64a6c7e5eeba7"
   },
-  {
-    "name": "bath_tone4",
-    "unicode": "1F6C0-1F3FE",
+  "bath_tone4": {
+    "category": "activity",
+    "moji": "🛀🏾",
+    "unicodeVersion": "8.0",
     "digest": "adad88c6830f31c4b5be194d1987d6aadf4adf45e4cb7f2e4657f0d20c0d663a"
   },
-  {
-    "name": "bath_tone5",
-    "unicode": "1F6C0-1F3FF",
+  "bath_tone5": {
+    "category": "activity",
+    "moji": "🛀🏿",
+    "unicodeVersion": "8.0",
     "digest": "952c4c9bf24e001e23a33ebf97bd92969cd9143e28ce93f9aafc708a8f966903"
   },
-  {
-    "name": "bathtub",
-    "unicode": "1F6C1",
+  "bathtub": {
+    "category": "objects",
+    "moji": "🛁",
+    "unicodeVersion": "6.0",
     "digest": "844dffb87ef872594195069b0d0df27c3fe51f3967ccbc8b2df811a086dd483a"
   },
-  {
-    "name": "battery",
-    "unicode": "1F50B",
+  "battery": {
+    "category": "objects",
+    "moji": "🔋",
+    "unicodeVersion": "6.0",
     "digest": "949ae06648667fb13d9121a6dfdd03bf8692794b28c36e9a8e8ac4515664449a"
   },
-  {
-    "name": "beach",
-    "unicode": "1F3D6",
-    "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
-  },
-  {
-    "name": "beach_with_umbrella",
-    "unicode": "1F3D6",
+  "beach": {
+    "category": "travel",
+    "moji": "🏖",
+    "unicodeVersion": "7.0",
     "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
   },
-  {
-    "name": "beach_umbrella",
-    "unicode": "26F1",
-    "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
-  },
-  {
-    "name": "umbrella_on_ground",
-    "unicode": "26F1",
+  "beach_umbrella": {
+    "category": "objects",
+    "moji": "⛱",
+    "unicodeVersion": "5.2",
     "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
   },
-  {
-    "name": "bear",
-    "unicode": "1F43B",
+  "bear": {
+    "category": "nature",
+    "moji": "🐻",
+    "unicodeVersion": "6.0",
     "digest": "a4b9066eaa5681e6af06e596a96a5217037460ffc3b013e8db4d34d762413246"
   },
-  {
-    "name": "bed",
-    "unicode": "1F6CF",
+  "bed": {
+    "category": "objects",
+    "moji": "🛏",
+    "unicodeVersion": "7.0",
     "digest": "08f6e20db51b1fb650b390a0a3074938646772f3fcee8c295d47742e44fe1e30"
   },
-  {
-    "name": "bee",
-    "unicode": "1F41D",
+  "bee": {
+    "category": "nature",
+    "moji": "🐝",
+    "unicodeVersion": "6.0",
     "digest": "5beb9a1650681b4adf69999d4808231c38f41a3ec693480b807cda86f964c570"
   },
-  {
-    "name": "beer",
-    "unicode": "1F37A",
+  "beer": {
+    "category": "food",
+    "moji": "🍺",
+    "unicodeVersion": "6.0",
     "digest": "69e227104976548ee0f37375fe1526fd65ef0a328d2d92db2feb1edfd7032bd4"
   },
-  {
-    "name": "beers",
-    "unicode": "1F37B",
+  "beers": {
+    "category": "food",
+    "moji": "🍻",
+    "unicodeVersion": "6.0",
     "digest": "db8b32d93bf6d161a3b027e55651d8f51231b13928b3610987ef62bb634d7501"
   },
-  {
-    "name": "beetle",
-    "unicode": "1F41E",
+  "beetle": {
+    "category": "nature",
+    "moji": "🐞",
+    "unicodeVersion": "6.0",
     "digest": "5aaa428e3f63f7cd1696839ab05be03fa0cd0cbed30a05c36cb270da330c3849"
   },
-  {
-    "name": "beginner",
-    "unicode": "1F530",
+  "beginner": {
+    "category": "symbols",
+    "moji": "🔰",
+    "unicodeVersion": "6.0",
     "digest": "2de4fdf92f182c42b12b7527034eaf767d996848b61f31ee69167728411ca0b1"
   },
-  {
-    "name": "bell",
-    "unicode": "1F514",
+  "bell": {
+    "category": "symbols",
+    "moji": "🔔",
+    "unicodeVersion": "6.0",
     "digest": "18d419417746ead408072b78fe2edb6314cdb49492873966fa9f9f06be09899b"
   },
-  {
-    "name": "bellhop",
-    "unicode": "1F6CE",
-    "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
-  },
-  {
-    "name": "bellhop_bell",
-    "unicode": "1F6CE",
+  "bellhop": {
+    "category": "objects",
+    "moji": "🛎",
+    "unicodeVersion": "7.0",
     "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
   },
-  {
-    "name": "bento",
-    "unicode": "1F371",
+  "bento": {
+    "category": "food",
+    "moji": "🍱",
+    "unicodeVersion": "6.0",
     "digest": "d46d4f681c5da7f7678b51be3445454a8ed18d917e132ae79077f05310e485f1"
   },
-  {
-    "name": "bicyclist",
-    "unicode": "1F6B4",
+  "bicyclist": {
+    "category": "activity",
+    "moji": "🚴",
+    "unicodeVersion": "6.0",
     "digest": "3302147b6b47c16adb97d78b7b761a1ca80e6d0b41d0b60f4da338d2f55f968b"
   },
-  {
-    "name": "bicyclist_tone1",
-    "unicode": "1F6B4-1F3FB",
+  "bicyclist_tone1": {
+    "category": "activity",
+    "moji": "🚴🏻",
+    "unicodeVersion": "8.0",
     "digest": "27eaae0eb61f5e7b3cd9faf02c042d6643a368051a7c9d7da4e0fb9802d39242"
   },
-  {
-    "name": "bicyclist_tone2",
-    "unicode": "1F6B4-1F3FC",
+  "bicyclist_tone2": {
+    "category": "activity",
+    "moji": "🚴🏼",
+    "unicodeVersion": "8.0",
     "digest": "39ee9e1071700da7079ad0146bf5711c3a222991eeca8b29b72a65677604444d"
   },
-  {
-    "name": "bicyclist_tone3",
-    "unicode": "1F6B4-1F3FD",
+  "bicyclist_tone3": {
+    "category": "activity",
+    "moji": "🚴🏽",
+    "unicodeVersion": "8.0",
     "digest": "03e1d2c4232c896147a9d4bf43becd61edbb5c84fc7193ecea474c0f9fb36817"
   },
-  {
-    "name": "bicyclist_tone4",
-    "unicode": "1F6B4-1F3FE",
+  "bicyclist_tone4": {
+    "category": "activity",
+    "moji": "🚴🏾",
+    "unicodeVersion": "8.0",
     "digest": "61393d9c4805be0379d86dd5bec9a1b02314433ab36cfd85bb48dfd073746617"
   },
-  {
-    "name": "bicyclist_tone5",
-    "unicode": "1F6B4-1F3FF",
+  "bicyclist_tone5": {
+    "category": "activity",
+    "moji": "🚴🏿",
+    "unicodeVersion": "8.0",
     "digest": "2b46d5f8303e5710dbf5db3a4edc9d88a032fe123fe79158024c9f51df5458c6"
   },
-  {
-    "name": "bike",
-    "unicode": "1F6B2",
+  "bike": {
+    "category": "travel",
+    "moji": "🚲",
+    "unicodeVersion": "6.0",
     "digest": "b41daa7c549d483e2336186a28baaa8ecb11986f490c0c54c793c44900c8f652"
   },
-  {
-    "name": "bikini",
-    "unicode": "1F459",
+  "bikini": {
+    "category": "people",
+    "moji": "👙",
+    "unicodeVersion": "6.0",
     "digest": "07fe156f64673818d69ce3bf03950ca59e3b5d346e45ca541da4078ab791f5ae"
   },
-  {
-    "name": "biohazard",
-    "unicode": "2623",
-    "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
-  },
-  {
-    "name": "biohazard_sign",
-    "unicode": "2623",
+  "biohazard": {
+    "category": "symbols",
+    "moji": "☣",
+    "unicodeVersion": "1.1",
     "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
   },
-  {
-    "name": "bird",
-    "unicode": "1F426",
+  "bird": {
+    "category": "nature",
+    "moji": "🐦",
+    "unicodeVersion": "6.0",
     "digest": "f916eaf8f271b3767ade9eabb69594c0479f45472d471cabaf59f6e965c161e0"
   },
-  {
-    "name": "birthday",
-    "unicode": "1F382",
+  "birthday": {
+    "category": "food",
+    "moji": "🎂",
+    "unicodeVersion": "6.0",
     "digest": "89e7c4c598ebee8ec8ab11ebe4ccc6defb7c4d2987ee2379a19b3b59827dd98a"
   },
-  {
-    "name": "black_circle",
-    "unicode": "26AB",
+  "black_circle": {
+    "category": "symbols",
+    "moji": "⚫",
+    "unicodeVersion": "4.1",
     "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
   },
-  {
-    "name": "black_heart",
-    "unicode": "1F5A4",
+  "black_heart": {
+    "category": "symbols",
+    "moji": "🖤",
+    "unicodeVersion": "9.0",
     "digest": "f334679168d6dd7328c28e9ae3cb2b1fca0e9c2777938d586bfe623db2a688b9"
   },
-  {
-    "name": "black_joker",
-    "unicode": "1F0CF",
+  "black_joker": {
+    "category": "symbols",
+    "moji": "🃏",
+    "unicodeVersion": "6.0",
     "digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d"
   },
-  {
-    "name": "black_large_square",
-    "unicode": "2B1B",
+  "black_large_square": {
+    "category": "symbols",
+    "moji": "⬛",
+    "unicodeVersion": "5.1",
     "digest": "cbd90dcbc2f674eafa53820548b5263c18c9845ab39937f085e85aca0aebb479"
   },
-  {
-    "name": "black_medium_small_square",
-    "unicode": "25FE",
+  "black_medium_small_square": {
+    "category": "symbols",
+    "moji": "◾",
+    "unicodeVersion": "3.2",
     "digest": "ab38363c2e862b8f67c719397a09a18e1ef996eec190691fdf769f5cfb209660"
   },
-  {
-    "name": "black_medium_square",
-    "unicode": "25FC",
+  "black_medium_square": {
+    "category": "symbols",
+    "moji": "◼",
+    "unicodeVersion": "3.2",
     "digest": "c9ffa87c37e8ee65fadcf755176949901aec7367e02abb85e63cad60cd922116"
   },
-  {
-    "name": "black_nib",
-    "unicode": "2712",
+  "black_nib": {
+    "category": "objects",
+    "moji": "✒",
+    "unicodeVersion": "1.1",
     "digest": "58fb23b1155102970eaa23765e7d529a21e8e545e076ec1158bf11b4de5f51a8"
   },
-  {
-    "name": "black_small_square",
-    "unicode": "25AA",
+  "black_small_square": {
+    "category": "symbols",
+    "moji": "▪",
+    "unicodeVersion": "1.1",
     "digest": "f69be6de578fffce5a3e60eda690104b2ef6a855c630040104fb760a02ff1aef"
   },
-  {
-    "name": "black_square_button",
-    "unicode": "1F532",
+  "black_square_button": {
+    "category": "symbols",
+    "moji": "🔲",
+    "unicodeVersion": "6.0",
     "digest": "9d818fcd08ed38cd0bbbcfd83e665aa29b3761c0d8b9806d8954d36785e267a8"
   },
-  {
-    "name": "blossom",
-    "unicode": "1F33C",
+  "blossom": {
+    "category": "nature",
+    "moji": "🌼",
+    "unicodeVersion": "6.0",
     "digest": "e8cf369d4e4cdb4eccc2ebcbb35439b0344221115701daae642e58dff8544922"
   },
-  {
-    "name": "blowfish",
-    "unicode": "1F421",
+  "blowfish": {
+    "category": "nature",
+    "moji": "🐡",
+    "unicodeVersion": "6.0",
     "digest": "e706849ed00f08a82312381c76f6f9ba6cc261fbf87a839c85e7dd54138f9dc3"
   },
-  {
-    "name": "blue_book",
-    "unicode": "1F4D8",
+  "blue_book": {
+    "category": "objects",
+    "moji": "📘",
+    "unicodeVersion": "6.0",
     "digest": "4c845748fe890516b32981b0b62bf3e8e9d906840c2060179f4f844100780615"
   },
-  {
-    "name": "blue_car",
-    "unicode": "1F699",
+  "blue_car": {
+    "category": "travel",
+    "moji": "🚙",
+    "unicodeVersion": "6.0",
     "digest": "eca91934eb5481726cfd897b1ed5eac306e14d02499fbe49316aaec6c72b6707"
   },
-  {
-    "name": "blue_heart",
-    "unicode": "1F499",
+  "blue_heart": {
+    "category": "symbols",
+    "moji": "💙",
+    "unicodeVersion": "6.0",
     "digest": "2caa0c8d18538cc871c6fe328a52f71e1df8aabf4d1cc2f5324b261d1b8cb99a"
   },
-  {
-    "name": "blush",
-    "unicode": "1F60A",
+  "blush": {
+    "category": "people",
+    "moji": "😊",
+    "unicodeVersion": "6.0",
     "digest": "3bfe8d603cfa39999c164779f666d39bbc507f124ba80233ee72da7b3b0c0457"
   },
-  {
-    "name": "boar",
-    "unicode": "1F417",
+  "boar": {
+    "category": "nature",
+    "moji": "🐗",
+    "unicodeVersion": "6.0",
     "digest": "c9d67479cace427ac3c30460fcffa1bf9a8e5262c0390962405dbbe6bf830fa6"
   },
-  {
-    "name": "bomb",
-    "unicode": "1F4A3",
+  "bomb": {
+    "category": "objects",
+    "moji": "💣",
+    "unicodeVersion": "6.0",
     "digest": "0155559abc4084f80e9b0b2a2091b8710ddd6369993b7fdd0685f4f8c2fd7e6c"
   },
-  {
-    "name": "book",
-    "unicode": "1F4D6",
+  "book": {
+    "category": "objects",
+    "moji": "📖",
+    "unicodeVersion": "6.0",
     "digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf"
   },
-  {
-    "name": "bookmark",
-    "unicode": "1F516",
+  "bookmark": {
+    "category": "objects",
+    "moji": "🔖",
+    "unicodeVersion": "6.0",
     "digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d"
   },
-  {
-    "name": "bookmark_tabs",
-    "unicode": "1F4D1",
+  "bookmark_tabs": {
+    "category": "objects",
+    "moji": "📑",
+    "unicodeVersion": "6.0",
     "digest": "c8fc7c9f3f82e1ccc97fc591345fdd88b09eec0fca428d8d4632a121cf1bc39a"
   },
-  {
-    "name": "books",
-    "unicode": "1F4DA",
+  "books": {
+    "category": "objects",
+    "moji": "📚",
+    "unicodeVersion": "6.0",
     "digest": "cbcf55d39dd05d26ef7350bc51e0e2f064f78bb8f59d407b516d63f68558f8e4"
   },
-  {
-    "name": "boom",
-    "unicode": "1F4A5",
+  "boom": {
+    "category": "nature",
+    "moji": "💥",
+    "unicodeVersion": "6.0",
     "digest": "f5400e9583f7f997cd2385f21379f6229424a9b221445bc8f36c0bb64bdb3168"
   },
-  {
-    "name": "boot",
-    "unicode": "1F462",
+  "boot": {
+    "category": "people",
+    "moji": "👢",
+    "unicodeVersion": "6.0",
     "digest": "b4706ff35909a6fb759a3b8a797e90cb67ffc60e4853386a7d89ace9693a9364"
   },
-  {
-    "name": "bouquet",
-    "unicode": "1F490",
+  "bouquet": {
+    "category": "nature",
+    "moji": "💐",
+    "unicodeVersion": "6.0",
     "digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f"
   },
-  {
-    "name": "bow",
-    "unicode": "1F647",
+  "bow": {
+    "category": "people",
+    "moji": "🙇",
+    "unicodeVersion": "6.0",
     "digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd"
   },
-  {
-    "name": "bow_and_arrow",
-    "unicode": "1F3F9",
-    "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
-  },
-  {
-    "name": "archery",
-    "unicode": "1F3F9",
+  "bow_and_arrow": {
+    "category": "activity",
+    "moji": "🏹",
+    "unicodeVersion": "8.0",
     "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
   },
-  {
-    "name": "bow_tone1",
-    "unicode": "1F647-1F3FB",
+  "bow_tone1": {
+    "category": "people",
+    "moji": "🙇🏻",
+    "unicodeVersion": "8.0",
     "digest": "995c8400ad60d5adc66c9ae5e3c0ecf56c48b478ad79418d45b6289933d25bdd"
   },
-  {
-    "name": "bow_tone2",
-    "unicode": "1F647-1F3FC",
+  "bow_tone2": {
+    "category": "people",
+    "moji": "🙇🏼",
+    "unicodeVersion": "8.0",
     "digest": "af89eec2fccda99d9bdd373b2345595882fee1c0a15d29af9028089e20255325"
   },
-  {
-    "name": "bow_tone3",
-    "unicode": "1F647-1F3FD",
+  "bow_tone3": {
+    "category": "people",
+    "moji": "🙇🏽",
+    "unicodeVersion": "8.0",
     "digest": "015d8122abdf2d0caa03815545f50fb7a71e05dacd46aaa133cc9ace5192f266"
   },
-  {
-    "name": "bow_tone4",
-    "unicode": "1F647-1F3FE",
+  "bow_tone4": {
+    "category": "people",
+    "moji": "🙇🏾",
+    "unicodeVersion": "8.0",
     "digest": "e8409096a795b775def654d36aeccb8eb91e83d7d1b32145cd73fd0b7b9e885c"
   },
-  {
-    "name": "bow_tone5",
-    "unicode": "1F647-1F3FF",
+  "bow_tone5": {
+    "category": "people",
+    "moji": "🙇🏿",
+    "unicodeVersion": "8.0",
     "digest": "d87042cde8dbad9fb1a91a2ec60116e27b4a76388b5779d771a0bbae12a2814d"
   },
-  {
-    "name": "bowling",
-    "unicode": "1F3B3",
+  "bowling": {
+    "category": "activity",
+    "moji": "🎳",
+    "unicodeVersion": "6.0",
     "digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662"
   },
-  {
-    "name": "boxing_glove",
-    "unicode": "1F94A",
-    "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
-  },
-  {
-    "name": "boxing_gloves",
-    "unicode": "1F94A",
+  "boxing_glove": {
+    "category": "activity",
+    "moji": "🥊",
+    "unicodeVersion": "9.0",
     "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
   },
-  {
-    "name": "boy",
-    "unicode": "1F466",
+  "boy": {
+    "category": "people",
+    "moji": "👦",
+    "unicodeVersion": "6.0",
     "digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1"
   },
-  {
-    "name": "boy_tone1",
-    "unicode": "1F466-1F3FB",
+  "boy_tone1": {
+    "category": "people",
+    "moji": "👦🏻",
+    "unicodeVersion": "8.0",
     "digest": "c0e2f0483715b239fe145b0056566f7a3a722319d9a87c1e66733dff1916a19f"
   },
-  {
-    "name": "boy_tone2",
-    "unicode": "1F466-1F3FC",
+  "boy_tone2": {
+    "category": "people",
+    "moji": "👦🏼",
+    "unicodeVersion": "8.0",
     "digest": "0001d0bd1ff4dbd898604ba965b4039d09667d955bc0349301b992f9ab6dd7fd"
   },
-  {
-    "name": "boy_tone3",
-    "unicode": "1F466-1F3FD",
+  "boy_tone3": {
+    "category": "people",
+    "moji": "👦🏽",
+    "unicodeVersion": "8.0",
     "digest": "e0f08755955fd2e0bd1c5d5e84429b2a234b24a744bb50bb9f1148495b2b29f9"
   },
-  {
-    "name": "boy_tone4",
-    "unicode": "1F466-1F3FE",
+  "boy_tone4": {
+    "category": "people",
+    "moji": "👦🏾",
+    "unicodeVersion": "8.0",
     "digest": "04b6bfee58a26b1ce2e5b403504a7033aaf395f03f5cd23e824f32c90c395fe6"
   },
-  {
-    "name": "boy_tone5",
-    "unicode": "1F466-1F3FF",
+  "boy_tone5": {
+    "category": "people",
+    "moji": "👦🏿",
+    "unicodeVersion": "8.0",
     "digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5"
   },
-  {
-    "name": "bread",
-    "unicode": "1F35E",
+  "bread": {
+    "category": "food",
+    "moji": "🍞",
+    "unicodeVersion": "6.0",
     "digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86"
   },
-  {
-    "name": "bride_with_veil",
-    "unicode": "1F470",
+  "bride_with_veil": {
+    "category": "people",
+    "moji": "👰",
+    "unicodeVersion": "6.0",
     "digest": "8e24bd91c3f564cf6148f2b3b4a7d692c11dd059e76a13331fdfb04ae060ea70"
   },
-  {
-    "name": "bride_with_veil_tone1",
-    "unicode": "1F470-1F3FB",
+  "bride_with_veil_tone1": {
+    "category": "people",
+    "moji": "👰🏻",
+    "unicodeVersion": "8.0",
     "digest": "0bd2f16f72586f50e768b14b9b353f2e98ccbb2581a568c33b06be56e70ca063"
   },
-  {
-    "name": "bride_with_veil_tone2",
-    "unicode": "1F470-1F3FC",
+  "bride_with_veil_tone2": {
+    "category": "people",
+    "moji": "👰🏼",
+    "unicodeVersion": "8.0",
     "digest": "e5463f811b2075754f0718b891757cd2e81071edf7af2215581227e1aad1d068"
   },
-  {
-    "name": "bride_with_veil_tone3",
-    "unicode": "1F470-1F3FD",
+  "bride_with_veil_tone3": {
+    "category": "people",
+    "moji": "👰🏽",
+    "unicodeVersion": "8.0",
     "digest": "e5a053a26f7ccebae7eb12f638be5ed80f77b744708d783eab2eb8aa091cf516"
   },
-  {
-    "name": "bride_with_veil_tone4",
-    "unicode": "1F470-1F3FE",
+  "bride_with_veil_tone4": {
+    "category": "people",
+    "moji": "👰🏾",
+    "unicodeVersion": "8.0",
     "digest": "410e23825e4401460946dc67a618bd3ace6e1a7c07dd88580a2349423685261f"
   },
-  {
-    "name": "bride_with_veil_tone5",
-    "unicode": "1F470-1F3FF",
+  "bride_with_veil_tone5": {
+    "category": "people",
+    "moji": "👰🏿",
+    "unicodeVersion": "8.0",
     "digest": "454e87e5a74e13e5b4993541231516fbbe6dbe9f990e1a6f3f4a744d7d4c1615"
   },
-  {
-    "name": "bridge_at_night",
-    "unicode": "1F309",
+  "bridge_at_night": {
+    "category": "travel",
+    "moji": "🌉",
+    "unicodeVersion": "6.0",
     "digest": "9d3cda5a59e27e3c90939f1ddbe7e998b3ea4fcacfa1467dea0edf39613c2d7f"
   },
-  {
-    "name": "briefcase",
-    "unicode": "1F4BC",
+  "briefcase": {
+    "category": "people",
+    "moji": "💼",
+    "unicodeVersion": "6.0",
     "digest": "9d00d6a92632aaadc71b017f448c883b27eb31a7554ebb51f7e3a9841f0f7f2b"
   },
-  {
-    "name": "broken_heart",
-    "unicode": "1F494",
+  "broken_heart": {
+    "category": "symbols",
+    "moji": "💔",
+    "unicodeVersion": "6.0",
     "digest": "c7ca53f444d72e596af46b61ffbc9e7c18a645020c22691e44f967db98dbf853"
   },
-  {
-    "name": "bug",
-    "unicode": "1F41B",
+  "bug": {
+    "category": "nature",
+    "moji": "🐛",
+    "unicodeVersion": "6.0",
     "digest": "0dccb1d5eb91769377b4c5b310f007b60f54a5c48ba9e467b3a06898a4831b90"
   },
-  {
-    "name": "bulb",
-    "unicode": "1F4A1",
+  "bulb": {
+    "category": "objects",
+    "moji": "💡",
+    "unicodeVersion": "6.0",
     "digest": "ccdaa2dfde5a88a347035a94b9d4d86cfc335ce0a73292423f5788a4bd21a5a8"
   },
-  {
-    "name": "bullettrain_front",
-    "unicode": "1F685",
+  "bullettrain_front": {
+    "category": "travel",
+    "moji": "🚅",
+    "unicodeVersion": "6.0",
     "digest": "5195a6a6d23f28e1aa5ebac6ede0f6c6a8b7ff33a9edf034814f227fe976177a"
   },
-  {
-    "name": "bullettrain_side",
-    "unicode": "1F684",
+  "bullettrain_side": {
+    "category": "travel",
+    "moji": "🚄",
+    "unicodeVersion": "6.0",
     "digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7"
   },
-  {
-    "name": "burrito",
-    "unicode": "1F32F",
+  "burrito": {
+    "category": "food",
+    "moji": "🌯",
+    "unicodeVersion": "8.0",
     "digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf"
   },
-  {
-    "name": "bus",
-    "unicode": "1F68C",
+  "bus": {
+    "category": "travel",
+    "moji": "🚌",
+    "unicodeVersion": "6.0",
     "digest": "192850b762edad21ac8770df38b9cae6d2bc1697a838462f3e36066bfb4eee50"
   },
-  {
-    "name": "busstop",
-    "unicode": "1F68F",
+  "busstop": {
+    "category": "travel",
+    "moji": "🚏",
+    "unicodeVersion": "6.0",
     "digest": "adabb1ec36402b33feb636eae3656e5a8b51ff1071bcb14125d8ab80d6d12d2a"
   },
-  {
-    "name": "bust_in_silhouette",
-    "unicode": "1F464",
+  "bust_in_silhouette": {
+    "category": "people",
+    "moji": "👤",
+    "unicodeVersion": "6.0",
     "digest": "277ae43301f1e49e0be03c8e52f0dc7b70c67f9d146bca0a14172e0098f115e6"
   },
-  {
-    "name": "busts_in_silhouette",
-    "unicode": "1F465",
+  "busts_in_silhouette": {
+    "category": "people",
+    "moji": "👥",
+    "unicodeVersion": "6.0",
     "digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b"
   },
-  {
-    "name": "butterfly",
-    "unicode": "1F98B",
+  "butterfly": {
+    "category": "nature",
+    "moji": "🦋",
+    "unicodeVersion": "9.0",
     "digest": "a91b6598c17b44a8dc8935a1d99e25f4483ea41470cdd2da343039a9eec29ef1"
   },
-  {
-    "name": "cactus",
-    "unicode": "1F335",
+  "cactus": {
+    "category": "nature",
+    "moji": "🌵",
+    "unicodeVersion": "6.0",
     "digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd"
   },
-  {
-    "name": "cake",
-    "unicode": "1F370",
+  "cake": {
+    "category": "food",
+    "moji": "🍰",
+    "unicodeVersion": "6.0",
     "digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b"
   },
-  {
-    "name": "calendar",
-    "unicode": "1F4C6",
+  "calendar": {
+    "category": "objects",
+    "moji": "📆",
+    "unicodeVersion": "6.0",
     "digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3"
   },
-  {
-    "name": "calendar_spiral",
-    "unicode": "1F5D3",
-    "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
-  },
-  {
-    "name": "spiral_calendar_pad",
-    "unicode": "1F5D3",
+  "calendar_spiral": {
+    "category": "objects",
+    "moji": "🗓",
+    "unicodeVersion": "7.0",
     "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
   },
-  {
-    "name": "call_me",
-    "unicode": "1F919",
-    "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
-  },
-  {
-    "name": "call_me_hand",
-    "unicode": "1F919",
+  "call_me": {
+    "category": "people",
+    "moji": "🤙",
+    "unicodeVersion": "9.0",
     "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
   },
-  {
-    "name": "call_me_tone1",
-    "unicode": "1F919-1F3FB",
-    "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
-  },
-  {
-    "name": "call_me_hand_tone1",
-    "unicode": "1F919-1F3FB",
+  "call_me_tone1": {
+    "category": "people",
+    "moji": "🤙🏻",
+    "unicodeVersion": "9.0",
     "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
   },
-  {
-    "name": "call_me_tone2",
-    "unicode": "1F919-1F3FC",
-    "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
-  },
-  {
-    "name": "call_me_hand_tone2",
-    "unicode": "1F919-1F3FC",
+  "call_me_tone2": {
+    "category": "people",
+    "moji": "🤙🏼",
+    "unicodeVersion": "9.0",
     "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
   },
-  {
-    "name": "call_me_tone3",
-    "unicode": "1F919-1F3FD",
-    "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
-  },
-  {
-    "name": "call_me_hand_tone3",
-    "unicode": "1F919-1F3FD",
+  "call_me_tone3": {
+    "category": "people",
+    "moji": "🤙🏽",
+    "unicodeVersion": "9.0",
     "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
   },
-  {
-    "name": "call_me_tone4",
-    "unicode": "1F919-1F3FE",
-    "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
-  },
-  {
-    "name": "call_me_hand_tone4",
-    "unicode": "1F919-1F3FE",
+  "call_me_tone4": {
+    "category": "people",
+    "moji": "🤙🏾",
+    "unicodeVersion": "9.0",
     "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
   },
-  {
-    "name": "call_me_tone5",
-    "unicode": "1F919-1F3FF",
-    "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
-  },
-  {
-    "name": "call_me_hand_tone5",
-    "unicode": "1F919-1F3FF",
+  "call_me_tone5": {
+    "category": "people",
+    "moji": "🤙🏿",
+    "unicodeVersion": "9.0",
     "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
   },
-  {
-    "name": "calling",
-    "unicode": "1F4F2",
+  "calling": {
+    "category": "objects",
+    "moji": "📲",
+    "unicodeVersion": "6.0",
     "digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91"
   },
-  {
-    "name": "camel",
-    "unicode": "1F42B",
+  "camel": {
+    "category": "nature",
+    "moji": "🐫",
+    "unicodeVersion": "6.0",
     "digest": "5f927927a7ab1277d0dc8b8211436957968b1e11365a8bf535e9bb94f92c5631"
   },
-  {
-    "name": "camera",
-    "unicode": "1F4F7",
+  "camera": {
+    "category": "objects",
+    "moji": "📷",
+    "unicodeVersion": "6.0",
     "digest": "fde03e396822a36cd6ae756ede885b945a074395264162731ca5db47a3b39d80"
   },
-  {
-    "name": "camera_with_flash",
-    "unicode": "1F4F8",
+  "camera_with_flash": {
+    "category": "objects",
+    "moji": "📸",
+    "unicodeVersion": "7.0",
     "digest": "9afd380208187780f00244c45d4db6c5ea1ea088d4a1bd8fc92a8f3877149750"
   },
-  {
-    "name": "camping",
-    "unicode": "1F3D5",
+  "camping": {
+    "category": "travel",
+    "moji": "🏕",
+    "unicodeVersion": "7.0",
     "digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9"
   },
-  {
-    "name": "cancer",
-    "unicode": "264B",
+  "cancer": {
+    "category": "symbols",
+    "moji": "♋",
+    "unicodeVersion": "1.1",
     "digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6"
   },
-  {
-    "name": "candle",
-    "unicode": "1F56F",
+  "candle": {
+    "category": "objects",
+    "moji": "🕯",
+    "unicodeVersion": "7.0",
     "digest": "211c04dc3a91b071c284d4180ed09f9d3320e3fd6ba8a9fddd0677bc97fd12cb"
   },
-  {
-    "name": "candy",
-    "unicode": "1F36C",
+  "candy": {
+    "category": "food",
+    "moji": "🍬",
+    "unicodeVersion": "6.0",
     "digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100"
   },
-  {
-    "name": "canoe",
-    "unicode": "1F6F6",
-    "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
-  },
-  {
-    "name": "kayak",
-    "unicode": "1F6F6",
+  "canoe": {
+    "category": "travel",
+    "moji": "🛶",
+    "unicodeVersion": "9.0",
     "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
   },
-  {
-    "name": "capital_abcd",
-    "unicode": "1F520",
+  "capital_abcd": {
+    "category": "symbols",
+    "moji": "🔠",
+    "unicodeVersion": "6.0",
     "digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa"
   },
-  {
-    "name": "capricorn",
-    "unicode": "2651",
+  "capricorn": {
+    "category": "symbols",
+    "moji": "♑",
+    "unicodeVersion": "1.1",
     "digest": "f11abad102603737b55486fe2ea4d01f28b203394bcd84f19a7948156e6c4b96"
   },
-  {
-    "name": "card_box",
-    "unicode": "1F5C3",
-    "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
-  },
-  {
-    "name": "card_file_box",
-    "unicode": "1F5C3",
+  "card_box": {
+    "category": "objects",
+    "moji": "🗃",
+    "unicodeVersion": "7.0",
     "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
   },
-  {
-    "name": "card_index",
-    "unicode": "1F4C7",
+  "card_index": {
+    "category": "objects",
+    "moji": "📇",
+    "unicodeVersion": "6.0",
     "digest": "86e187e0a72ca5d00207d6ef34d66ce15046848a831c2b5184fb840c5332a2a8"
   },
-  {
-    "name": "carousel_horse",
-    "unicode": "1F3A0",
+  "carousel_horse": {
+    "category": "travel",
+    "moji": "🎠",
+    "unicodeVersion": "6.0",
     "digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe"
   },
-  {
-    "name": "carrot",
-    "unicode": "1F955",
+  "carrot": {
+    "category": "food",
+    "moji": "🥕",
+    "unicodeVersion": "9.0",
     "digest": "3a6fd98b63ee73d982a9cdacb08cf7b4014368cde8ffce6056b7df25a5a472b1"
   },
-  {
-    "name": "cartwheel",
-    "unicode": "1F938",
-    "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
-  },
-  {
-    "name": "person_doing_cartwheel",
-    "unicode": "1F938",
+  "cartwheel": {
+    "category": "activity",
+    "moji": "🤸",
+    "unicodeVersion": "9.0",
     "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
   },
-  {
-    "name": "cartwheel_tone1",
-    "unicode": "1F938-1F3FB",
-    "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
-  },
-  {
-    "name": "person_doing_cartwheel_tone1",
-    "unicode": "1F938-1F3FB",
+  "cartwheel_tone1": {
+    "category": "activity",
+    "moji": "🤸🏻",
+    "unicodeVersion": "9.0",
     "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
   },
-  {
-    "name": "cartwheel_tone2",
-    "unicode": "1F938-1F3FC",
-    "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
-  },
-  {
-    "name": "person_doing_cartwheel_tone2",
-    "unicode": "1F938-1F3FC",
+  "cartwheel_tone2": {
+    "category": "activity",
+    "moji": "🤸🏼",
+    "unicodeVersion": "9.0",
     "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
   },
-  {
-    "name": "cartwheel_tone3",
-    "unicode": "1F938-1F3FD",
-    "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
-  },
-  {
-    "name": "person_doing_cartwheel_tone3",
-    "unicode": "1F938-1F3FD",
+  "cartwheel_tone3": {
+    "category": "activity",
+    "moji": "🤸🏽",
+    "unicodeVersion": "9.0",
     "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
   },
-  {
-    "name": "cartwheel_tone4",
-    "unicode": "1F938-1F3FE",
-    "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
-  },
-  {
-    "name": "person_doing_cartwheel_tone4",
-    "unicode": "1F938-1F3FE",
+  "cartwheel_tone4": {
+    "category": "activity",
+    "moji": "🤸🏾,",
+    "unicodeVersion": "9.0",
     "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
   },
-  {
-    "name": "cartwheel_tone5",
-    "unicode": "1F938-1F3FF",
-    "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
-  },
-  {
-    "name": "person_doing_cartwheel_tone5",
-    "unicode": "1F938-1F3FF",
+  "cartwheel_tone5": {
+    "category": "activity",
+    "moji": "🤸🏿",
+    "unicodeVersion": "9.0",
     "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
   },
-  {
-    "name": "cat",
-    "unicode": "1F431",
+  "cat": {
+    "category": "nature",
+    "moji": "🐱",
+    "unicodeVersion": "6.0",
     "digest": "e52d0d3a205a0ba99094717e171a7f572b713a0e21b276ffa4a826596fe5cafc"
   },
-  {
-    "name": "cat2",
-    "unicode": "1F408",
+  "cat2": {
+    "category": "nature",
+    "moji": "🐈",
+    "unicodeVersion": "6.0",
     "digest": "46aa67a99f782935932c77b8de93287142297abe52928c173191cf55bb8f4339"
   },
-  {
-    "name": "cd",
-    "unicode": "1F4BF",
+  "cd": {
+    "category": "objects",
+    "moji": "💿",
+    "unicodeVersion": "6.0",
     "digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b"
   },
-  {
-    "name": "chains",
-    "unicode": "26D3",
+  "chains": {
+    "category": "objects",
+    "moji": "⛓",
+    "unicodeVersion": "5.2",
     "digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2"
   },
-  {
-    "name": "champagne",
-    "unicode": "1F37E",
-    "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
-  },
-  {
-    "name": "bottle_with_popping_cork",
-    "unicode": "1F37E",
+  "champagne": {
+    "category": "food",
+    "moji": "🍾",
+    "unicodeVersion": "8.0",
     "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
   },
-  {
-    "name": "champagne_glass",
-    "unicode": "1F942",
-    "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
-  },
-  {
-    "name": "clinking_glass",
-    "unicode": "1F942",
+  "champagne_glass": {
+    "category": "food",
+    "moji": "🥂",
+    "unicodeVersion": "9.0",
     "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
   },
-  {
-    "name": "chart",
-    "unicode": "1F4B9",
+  "chart": {
+    "category": "symbols",
+    "moji": "💹",
+    "unicodeVersion": "6.0",
     "digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f"
   },
-  {
-    "name": "chart_with_downwards_trend",
-    "unicode": "1F4C9",
+  "chart_with_downwards_trend": {
+    "category": "objects",
+    "moji": "📉",
+    "unicodeVersion": "6.0",
     "digest": "5db7ccbc37665736a9c0b2f50247dcc09e404ec37f39db45b7b8b9464172a18c"
   },
-  {
-    "name": "chart_with_upwards_trend",
-    "unicode": "1F4C8",
+  "chart_with_upwards_trend": {
+    "category": "objects",
+    "moji": "📈",
+    "unicodeVersion": "6.0",
     "digest": "bc4ea250b102fe5c09847e471478aff065ad3df755d9717896d38d887d9c6733"
   },
-  {
-    "name": "checkered_flag",
-    "unicode": "1F3C1",
+  "checkered_flag": {
+    "category": "travel",
+    "moji": "🏁",
+    "unicodeVersion": "6.0",
     "digest": "0e77180e0cf9fc87e755a5a42cf23aec6bf30931db41331311e97ba0be178b78"
   },
-  {
-    "name": "cheese",
-    "unicode": "1F9C0",
-    "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
-  },
-  {
-    "name": "cheese_wedge",
-    "unicode": "1F9C0",
+  "cheese": {
+    "category": "food",
+    "moji": "🧀",
+    "unicodeVersion": "8.0",
     "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
   },
-  {
-    "name": "cherries",
-    "unicode": "1F352",
+  "cherries": {
+    "category": "food",
+    "moji": "🍒",
+    "unicodeVersion": "6.0",
     "digest": "13b8db9e7e6eec8509aa80c762966e1bf3538fcb1ac3d6eab18ee4da1528cf84"
   },
-  {
-    "name": "cherry_blossom",
-    "unicode": "1F338",
+  "cherry_blossom": {
+    "category": "nature",
+    "moji": "🌸",
+    "unicodeVersion": "6.0",
     "digest": "af3083f5f8dd94936113f2e16caba5aec7a774d5589aa08bf5de82a2d278cc66"
   },
-  {
-    "name": "chestnut",
-    "unicode": "1F330",
+  "chestnut": {
+    "category": "nature",
+    "moji": "🌰",
+    "unicodeVersion": "6.0",
     "digest": "9f85b79b207a69ab81ab88dcef04954000965b039b4cf57de5f1b381745ab98b"
   },
-  {
-    "name": "chicken",
-    "unicode": "1F414",
+  "chicken": {
+    "category": "nature",
+    "moji": "🐔",
+    "unicodeVersion": "6.0",
     "digest": "57ceb4459d183740009caac6ebed089d2f1e12f67c138e1be1d0f992313c0ac4"
   },
-  {
-    "name": "children_crossing",
-    "unicode": "1F6B8",
+  "children_crossing": {
+    "category": "symbols",
+    "moji": "🚸",
+    "unicodeVersion": "6.0",
     "digest": "0ded7d9aca0161e8ef8e2858c3c198e70e4badc7105ac3a6886e06975de19106"
   },
-  {
-    "name": "chipmunk",
-    "unicode": "1F43F",
+  "chipmunk": {
+    "category": "nature",
+    "moji": "🐿",
+    "unicodeVersion": "7.0",
     "digest": "5b0dc1a859163097727ba2ba5ffca38b0a54d925eebb089977d28d0b4d917a3f"
   },
-  {
-    "name": "chocolate_bar",
-    "unicode": "1F36B",
+  "chocolate_bar": {
+    "category": "food",
+    "moji": "🍫",
+    "unicodeVersion": "6.0",
     "digest": "dd273e5050488acaf885f8a18b6e2b3901f69c5b39fa6465fb60621783d4109a"
   },
-  {
-    "name": "christmas_tree",
-    "unicode": "1F384",
+  "christmas_tree": {
+    "category": "nature",
+    "moji": "🎄",
+    "unicodeVersion": "6.0",
     "digest": "ce60cbe2ebbe8057be8edea2392455fedd2bcda64a0a831f6a1942028af7e747"
   },
-  {
-    "name": "church",
-    "unicode": "26EA",
+  "church": {
+    "category": "travel",
+    "moji": "⛪",
+    "unicodeVersion": "5.2",
     "digest": "2c328456528f7336e59443e20ec3ab22fe71f1fccb1dd50d0ad68eb206937557"
   },
-  {
-    "name": "cinema",
-    "unicode": "1F3A6",
+  "cinema": {
+    "category": "symbols",
+    "moji": "🎦",
+    "unicodeVersion": "6.0",
     "digest": "4c26dcdc76f93dbc2a1dc49ed4e132b8e8f2b7cdc1acf5e09b3dfd99430d97cd"
   },
-  {
-    "name": "circus_tent",
-    "unicode": "1F3AA",
+  "circus_tent": {
+    "category": "activity",
+    "moji": "🎪",
+    "unicodeVersion": "6.0",
     "digest": "fec5f2a06222be8be549178b29720343cc00145177ec387ca4e6f3432481fe77"
   },
-  {
-    "name": "city_dusk",
-    "unicode": "1F306",
+  "city_dusk": {
+    "category": "travel",
+    "moji": "🌆",
+    "unicodeVersion": "6.0",
     "digest": "bba345e949dcc51f5f018220f000223797970c82ead2ab9c822f9dc0847aa155"
   },
-  {
-    "name": "city_sunset",
-    "unicode": "1F307",
-    "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
-  },
-  {
-    "name": "city_sunrise",
-    "unicode": "1F307",
+  "city_sunset": {
+    "category": "travel",
+    "moji": "🌇",
+    "unicodeVersion": "6.0",
     "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
   },
-  {
-    "name": "cityscape",
-    "unicode": "1F3D9",
+  "cityscape": {
+    "category": "travel",
+    "moji": "🏙",
+    "unicodeVersion": "7.0",
     "digest": "ee360be7514c4bfb0d539dd28f3b2031ebcef04e850723ec0685fb54bd8e6d5f"
   },
-  {
-    "name": "cl",
-    "unicode": "1F191",
+  "cl": {
+    "category": "symbols",
+    "moji": "🆑",
+    "unicodeVersion": "6.0",
     "digest": "fcec2855dbad9fda11d6e2802bc0dcaabab0b5be233508f5e439f156f07602c1"
   },
-  {
-    "name": "clap",
-    "unicode": "1F44F",
+  "clap": {
+    "category": "people",
+    "moji": "👏",
+    "unicodeVersion": "6.0",
     "digest": "a1860ce7812a9f6fb55e45761e1b79a2f8f0620eb04f80748a38420889d58a2a"
   },
-  {
-    "name": "clap_tone1",
-    "unicode": "1F44F-1F3FB",
+  "clap_tone1": {
+    "category": "people",
+    "moji": "👏🏻",
+    "unicodeVersion": "8.0",
     "digest": "18a7022e08223fb2109af5a9b9a5b4f47dc870ce4453f4987d2d0b729ef54586"
   },
-  {
-    "name": "clap_tone2",
-    "unicode": "1F44F-1F3FC",
+  "clap_tone2": {
+    "category": "people",
+    "moji": "👏🏼",
+    "unicodeVersion": "8.0",
     "digest": "5954c8658b15e755d2018d8674df84d38e22ffededc4d726c6a33b709f71426a"
   },
-  {
-    "name": "clap_tone3",
-    "unicode": "1F44F-1F3FD",
+  "clap_tone3": {
+    "category": "people",
+    "moji": "👏🏽",
+    "unicodeVersion": "8.0",
     "digest": "22639b6bd3c53784a2f855d6db7bdf31621519f19dfc29a6bc310eee6421f742"
   },
-  {
-    "name": "clap_tone4",
-    "unicode": "1F44F-1F3FE",
+  "clap_tone4": {
+    "category": "people",
+    "moji": "👏🏾",
+    "unicodeVersion": "8.0",
     "digest": "e55248dc163d1bbd118b50cd8767750ead86d082151febbc0a75b32d63abceec"
   },
-  {
-    "name": "clap_tone5",
-    "unicode": "1F44F-1F3FF",
+  "clap_tone5": {
+    "category": "people",
+    "moji": "👏🏿",
+    "unicodeVersion": "8.0",
     "digest": "76046b8157dabbe048a07fc318122456020c9c980fc1b8ab76802330e07b3b53"
   },
-  {
-    "name": "clapper",
-    "unicode": "1F3AC",
+  "clapper": {
+    "category": "activity",
+    "moji": "🎬",
+    "unicodeVersion": "6.0",
     "digest": "8149752a0e3e8abede2d433d1afab6d217877d0c76adb1e2845a0142c0cdcbaa"
   },
-  {
-    "name": "classical_building",
-    "unicode": "1F3DB",
+  "classical_building": {
+    "category": "travel",
+    "moji": "🏛",
+    "unicodeVersion": "7.0",
     "digest": "9ee0d00c43d6e22b6a3ddea67619737270cc7e9294797a19c7c60d5f92aa44fa"
   },
-  {
-    "name": "clipboard",
-    "unicode": "1F4CB",
+  "clipboard": {
+    "category": "objects",
+    "moji": "📋",
+    "unicodeVersion": "6.0",
     "digest": "bdd7f7d973c714e59d2903d401a876e6018794c7987c9ca57108c137c5edc25f"
   },
-  {
-    "name": "clock",
-    "unicode": "1F570",
-    "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
-  },
-  {
-    "name": "mantlepiece_clock",
-    "unicode": "1F570",
+  "clock": {
+    "category": "objects",
+    "moji": "🕰",
+    "unicodeVersion": "7.0",
     "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
   },
-  {
-    "name": "clock1",
-    "unicode": "1F550",
+  "clock1": {
+    "category": "symbols",
+    "moji": "🕐",
+    "unicodeVersion": "6.0",
     "digest": "1778eec07ce061c9393e5abee5ca83b24e1ce61d8a75fa2e39efcb31aa160395"
   },
-  {
-    "name": "clock10",
-    "unicode": "1F559",
+  "clock10": {
+    "category": "symbols",
+    "moji": "🕙",
+    "unicodeVersion": "6.0",
     "digest": "601fc12ea5280a54c2e69dbb685f454e4165fe771756ed6f89016e29e683a24f"
   },
-  {
-    "name": "clock1030",
-    "unicode": "1F565",
+  "clock1030": {
+    "category": "symbols",
+    "moji": "🕥",
+    "unicodeVersion": "6.0",
     "digest": "4fd155f08f797542d52cff4b0aa3ca9f080f37a41c301b82f90ff6d4693c890e"
   },
-  {
-    "name": "clock11",
-    "unicode": "1F55A",
+  "clock11": {
+    "category": "symbols",
+    "moji": "🕚",
+    "unicodeVersion": "6.0",
     "digest": "5c79dc812e812e8a01993ea633b323d654ce3a7ea258692781a4896e4ad2017e"
   },
-  {
-    "name": "clock1130",
-    "unicode": "1F566",
+  "clock1130": {
+    "category": "symbols",
+    "moji": "🕦",
+    "unicodeVersion": "6.0",
     "digest": "41497ee2020ee5ac9aa5f9b07560f7afca7c422b04214449cfc5cea9f020f52e"
   },
-  {
-    "name": "clock12",
-    "unicode": "1F55B",
+  "clock12": {
+    "category": "symbols",
+    "moji": "🕛",
+    "unicodeVersion": "6.0",
     "digest": "046bb7ffa5f5d27c2e3411ba543484d9dabb8ebf6d6e7a7e9bfb088c1813500c"
   },
-  {
-    "name": "clock1230",
-    "unicode": "1F567",
+  "clock1230": {
+    "category": "symbols",
+    "moji": "🕧",
+    "unicodeVersion": "6.0",
     "digest": "bbfe9db5a2043aaba19a7a2a0185c7efcebf1e8c9263b8233f75b53c4825f0f4"
   },
-  {
-    "name": "clock130",
-    "unicode": "1F55C",
+  "clock130": {
+    "category": "symbols",
+    "moji": "🕜",
+    "unicodeVersion": "6.0",
     "digest": "8662cb395ee680c2781123305c4c8ce8c0df9565c2c942668940be540cc0c094"
   },
-  {
-    "name": "clock2",
-    "unicode": "1F551",
+  "clock2": {
+    "category": "symbols",
+    "moji": "🕑",
+    "unicodeVersion": "6.0",
     "digest": "42f7429748b612dce7de77221cbbc710655811f7bb23e2a986c36e6d662f0ec4"
   },
-  {
-    "name": "clock230",
-    "unicode": "1F55D",
+  "clock230": {
+    "category": "symbols",
+    "moji": "🕝",
+    "unicodeVersion": "6.0",
     "digest": "e710b6ef14227cd240ea3e2a867c8ef45b5c060adf3cb30ba9077c2351fe6677"
   },
-  {
-    "name": "clock3",
-    "unicode": "1F552",
+  "clock3": {
+    "category": "symbols",
+    "moji": "🕒",
+    "unicodeVersion": "6.0",
     "digest": "7340d465b398a378211dff9ec806db579d061206fd6fc238623d070cfe0a55ce"
   },
-  {
-    "name": "clock330",
-    "unicode": "1F55E",
+  "clock330": {
+    "category": "symbols",
+    "moji": "🕞",
+    "unicodeVersion": "6.0",
     "digest": "7aa4a15cc8de04ed3bdeb0f8a54a7915065f2809a07054e002d89926c9766831"
   },
-  {
-    "name": "clock4",
-    "unicode": "1F553",
+  "clock4": {
+    "category": "symbols",
+    "moji": "🕓",
+    "unicodeVersion": "6.0",
     "digest": "36fd88e81ad488b0ec49a911a838693281573fa14736ae4a6dd1c40a4ff69bb1"
   },
-  {
-    "name": "clock430",
-    "unicode": "1F55F",
+  "clock430": {
+    "category": "symbols",
+    "moji": "🕟",
+    "unicodeVersion": "6.0",
     "digest": "7bd5dd71e89d95dcf18b9e8c1fe2a353a7da3b69aadb8dda80ee9bafb05da58d"
   },
-  {
-    "name": "clock5",
-    "unicode": "1F554",
+  "clock5": {
+    "category": "symbols",
+    "moji": "🕔",
+    "unicodeVersion": "6.0",
     "digest": "aa406409e56a0bfd8c850e44efe45fd190ffd7bf7061e934ed7928dfbdfc9eba"
   },
-  {
-    "name": "clock530",
-    "unicode": "1F560",
+  "clock530": {
+    "category": "symbols",
+    "moji": "🕠",
+    "unicodeVersion": "6.0",
     "digest": "25dd3bcc53ddd98eeea498d7dbd4c306ef39dd033f15909063388a0800febf41"
   },
-  {
-    "name": "clock6",
-    "unicode": "1F555",
+  "clock6": {
+    "category": "symbols",
+    "moji": "🕕",
+    "unicodeVersion": "6.0",
     "digest": "0a321eaf1bc5db8436bbadac66c45ba257fc98ad4c7569ce3fc6602c824b6d7c"
   },
-  {
-    "name": "clock630",
-    "unicode": "1F561",
+  "clock630": {
+    "category": "symbols",
+    "moji": "🕡",
+    "unicodeVersion": "6.0",
     "digest": "55a4c5a665fdd38a724e9357a93c55401fcd5f1b13078c25754bd70c3fc4ccec"
   },
-  {
-    "name": "clock7",
-    "unicode": "1F556",
+  "clock7": {
+    "category": "symbols",
+    "moji": "🕖",
+    "unicodeVersion": "6.0",
     "digest": "6154306545716e865da0ec537ee4f22bfe6c7294502a64a2dcf425c587d0e2a2"
   },
-  {
-    "name": "clock730",
-    "unicode": "1F562",
+  "clock730": {
+    "category": "symbols",
+    "moji": "🕢",
+    "unicodeVersion": "6.0",
     "digest": "6925654de642e50f84661f94364a96c87757d73fffe766aacbf4bbd70130547b"
   },
-  {
-    "name": "clock8",
-    "unicode": "1F557",
+  "clock8": {
+    "category": "symbols",
+    "moji": "🕗",
+    "unicodeVersion": "6.0",
     "digest": "9be2d189c7ea56d39fd259f84853d753c1cf33e64f8ed57f86f822d9ae23a1ee"
   },
-  {
-    "name": "clock830",
-    "unicode": "1F563",
+  "clock830": {
+    "category": "symbols",
+    "moji": "🕣",
+    "unicodeVersion": "6.0",
     "digest": "16878613c0000d2f558c88d080551f424a8bd9df1358e0f931dd25c3da68f2d9"
   },
-  {
-    "name": "clock9",
-    "unicode": "1F558",
+  "clock9": {
+    "category": "symbols",
+    "moji": "🕘",
+    "unicodeVersion": "6.0",
     "digest": "1d1e7e3c9d085ffa5b7c0f3d9fd394b734f16ae3b60df09af50fe6c8d4f3c8bb"
   },
-  {
-    "name": "clock930",
-    "unicode": "1F564",
+  "clock930": {
+    "category": "symbols",
+    "moji": "🕤",
+    "unicodeVersion": "6.0",
     "digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74"
   },
-  {
-    "name": "closed_book",
-    "unicode": "1F4D5",
+  "closed_book": {
+    "category": "objects",
+    "moji": "📕",
+    "unicodeVersion": "6.0",
     "digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f"
   },
-  {
-    "name": "closed_lock_with_key",
-    "unicode": "1F510",
+  "closed_lock_with_key": {
+    "category": "objects",
+    "moji": "🔐",
+    "unicodeVersion": "6.0",
     "digest": "e39adfe9b30973bca16472c2b7e6462b064a93b9d452aa48edd74c727641a83d"
   },
-  {
-    "name": "closed_umbrella",
-    "unicode": "1F302",
+  "closed_umbrella": {
+    "category": "people",
+    "moji": "🌂",
+    "unicodeVersion": "6.0",
     "digest": "2cc0592c74601f7439e88c3c1ec4f05e3459608ef1ea6558c5824ed7c3889727"
   },
-  {
-    "name": "cloud",
-    "unicode": "2601",
+  "cloud": {
+    "category": "nature",
+    "moji": "☁",
+    "unicodeVersion": "1.1",
     "digest": "5b3a19718dfa8a381929665afdc2284464d24020c8dd0caff4dad465a1f536ba"
   },
-  {
-    "name": "cloud_lightning",
-    "unicode": "1F329",
-    "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
-  },
-  {
-    "name": "cloud_with_lightning",
-    "unicode": "1F329",
+  "cloud_lightning": {
+    "category": "nature",
+    "moji": "🌩",
+    "unicodeVersion": "7.0",
     "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
   },
-  {
-    "name": "cloud_rain",
-    "unicode": "1F327",
-    "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
-  },
-  {
-    "name": "cloud_with_rain",
-    "unicode": "1F327",
+  "cloud_rain": {
+    "category": "nature",
+    "moji": "🌧",
+    "unicodeVersion": "7.0",
     "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
   },
-  {
-    "name": "cloud_snow",
-    "unicode": "1F328",
-    "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
-  },
-  {
-    "name": "cloud_with_snow",
-    "unicode": "1F328",
+  "cloud_snow": {
+    "category": "nature",
+    "moji": "🌨",
+    "unicodeVersion": "7.0",
     "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
   },
-  {
-    "name": "cloud_tornado",
-    "unicode": "1F32A",
-    "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
-  },
-  {
-    "name": "cloud_with_tornado",
-    "unicode": "1F32A",
+  "cloud_tornado": {
+    "category": "nature",
+    "moji": "🌪",
+    "unicodeVersion": "7.0",
     "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
   },
-  {
-    "name": "clown",
-    "unicode": "1F921",
-    "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
-  },
-  {
-    "name": "clown_face",
-    "unicode": "1F921",
+  "clown": {
+    "category": "people",
+    "moji": "🤡",
+    "unicodeVersion": "9.0",
     "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
   },
-  {
-    "name": "clubs",
-    "unicode": "2663",
+  "clubs": {
+    "category": "symbols",
+    "moji": "♣",
+    "unicodeVersion": "1.1",
     "digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138"
   },
-  {
-    "name": "cocktail",
-    "unicode": "1F378",
+  "cocktail": {
+    "category": "food",
+    "moji": "🍸",
+    "unicodeVersion": "6.0",
     "digest": "3792def2cde885cf32167f04904d3b0b788388e8af410c63e4cd31550feba775"
   },
-  {
-    "name": "coffee",
-    "unicode": "2615",
+  "coffee": {
+    "category": "food",
+    "moji": "☕",
+    "unicodeVersion": "4.0",
     "digest": "0d29615a7a67d3aafa257b909bb915dc74fa8f854acb0d9a2c29e94eedf80326"
   },
-  {
-    "name": "coffin",
-    "unicode": "26B0",
+  "coffin": {
+    "category": "objects",
+    "moji": "⚰",
+    "unicodeVersion": "4.1",
     "digest": "78eccc1aad2a822649fba8503d4d30354bef367c4271193c40ddb692308f9db8"
   },
-  {
-    "name": "cold_sweat",
-    "unicode": "1F630",
+  "cold_sweat": {
+    "category": "people",
+    "moji": "😰",
+    "unicodeVersion": "6.0",
     "digest": "f53aab523ed3fa2224a16881d263fb5e039f163380f92feb2c63c20f9b14dcd2"
   },
-  {
-    "name": "comet",
-    "unicode": "2604",
+  "comet": {
+    "category": "nature",
+    "moji": "☄",
+    "unicodeVersion": "1.1",
     "digest": "40ce93e55c6e57a88d80670b37171190bd5ffc87b7078891d8de5b15795385c5"
   },
-  {
-    "name": "compression",
-    "unicode": "1F5DC",
+  "compression": {
+    "category": "objects",
+    "moji": "🗜",
+    "unicodeVersion": "7.0",
     "digest": "c8841f7afb5345f1c31da116a7fb41d07232ea58d3f7f1a75c5890aa1a80bfd6"
   },
-  {
-    "name": "computer",
-    "unicode": "1F4BB",
+  "computer": {
+    "category": "objects",
+    "moji": "💻",
+    "unicodeVersion": "6.0",
     "digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93"
   },
-  {
-    "name": "confetti_ball",
-    "unicode": "1F38A",
+  "confetti_ball": {
+    "category": "objects",
+    "moji": "🎊",
+    "unicodeVersion": "6.0",
     "digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10"
   },
-  {
-    "name": "confounded",
-    "unicode": "1F616",
+  "confounded": {
+    "category": "people",
+    "moji": "😖",
+    "unicodeVersion": "6.0",
     "digest": "e2ff3b4df65d00c1ca9ae0cb379f959ea2cecefb3d676d4f8c2c5f2c103da4f6"
   },
-  {
-    "name": "confused",
-    "unicode": "1F615",
+  "confused": {
+    "category": "people",
+    "moji": "😕",
+    "unicodeVersion": "6.1",
     "digest": "118d7f830ec08a3ac4b798eebb77a989b8c142f2588727181be4a2548e3c4f06"
   },
-  {
-    "name": "congratulations",
-    "unicode": "3297",
+  "congratulations": {
+    "category": "symbols",
+    "moji": "㊗",
+    "unicodeVersion": "1.1",
     "digest": "02fd1338c54fe5f9a0fd861f23c56edc1d39bcd3140b68f0f626f9e2494d2d1c"
   },
-  {
-    "name": "construction",
-    "unicode": "1F6A7",
+  "construction": {
+    "category": "travel",
+    "moji": "🚧",
+    "unicodeVersion": "6.0",
     "digest": "c3a0401331111b9eda1206bee5f322db80b0870547d307b10dcac1314e4078c8"
   },
-  {
-    "name": "construction_site",
-    "unicode": "1F3D7",
-    "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
-  },
-  {
-    "name": "building_construction",
-    "unicode": "1F3D7",
+  "construction_site": {
+    "category": "travel",
+    "moji": "🏗",
+    "unicodeVersion": "7.0",
     "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
   },
-  {
-    "name": "construction_worker",
-    "unicode": "1F477",
+  "construction_worker": {
+    "category": "people",
+    "moji": "👷",
+    "unicodeVersion": "6.0",
     "digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6"
   },
-  {
-    "name": "construction_worker_tone1",
-    "unicode": "1F477-1F3FB",
+  "construction_worker_tone1": {
+    "category": "people",
+    "moji": "👷🏻",
+    "unicodeVersion": "8.0",
     "digest": "fcd927405fef4486105cd3aff62155467d21cebbc013924d4b52b717b566602b"
   },
-  {
-    "name": "construction_worker_tone2",
-    "unicode": "1F477-1F3FC",
+  "construction_worker_tone2": {
+    "category": "people",
+    "moji": "👷🏼",
+    "unicodeVersion": "8.0",
     "digest": "d1ec773828936c703dd6e334e696dc3cf7c34c0a8ec691564a384b735cdeaaba"
   },
-  {
-    "name": "construction_worker_tone3",
-    "unicode": "1F477-1F3FD",
+  "construction_worker_tone3": {
+    "category": "people",
+    "moji": "👷🏽",
+    "unicodeVersion": "8.0",
     "digest": "37c114d6879b9b32b800b0d4cf770dcbe04d1455698130ecd709a0cb9dea880b"
   },
-  {
-    "name": "construction_worker_tone4",
-    "unicode": "1F477-1F3FE",
+  "construction_worker_tone4": {
+    "category": "people",
+    "moji": "👷🏾",
+    "unicodeVersion": "8.0",
     "digest": "5264996c1bedb6061a0dfdddce233d863bf308d27127ad152b63bfd983162cf7"
   },
-  {
-    "name": "construction_worker_tone5",
-    "unicode": "1F477-1F3FF",
+  "construction_worker_tone5": {
+    "category": "people",
+    "moji": "👷🏿",
+    "unicodeVersion": "8.0",
     "digest": "87051aec81fd5dfd4dc44ff0411a528ee08253e9494d37efa550694e28dde6d3"
   },
-  {
-    "name": "control_knobs",
-    "unicode": "1F39B",
+  "control_knobs": {
+    "category": "objects",
+    "moji": "🎛",
+    "unicodeVersion": "7.0",
     "digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb"
   },
-  {
-    "name": "convenience_store",
-    "unicode": "1F3EA",
+  "convenience_store": {
+    "category": "travel",
+    "moji": "🏪",
+    "unicodeVersion": "6.0",
     "digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52"
   },
-  {
-    "name": "cookie",
-    "unicode": "1F36A",
+  "cookie": {
+    "category": "food",
+    "moji": "🍪",
+    "unicodeVersion": "6.0",
     "digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4"
   },
-  {
-    "name": "cooking",
-    "unicode": "1F373",
+  "cooking": {
+    "category": "food",
+    "moji": "🍳",
+    "unicodeVersion": "6.0",
     "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58"
   },
-  {
-    "name": "cool",
-    "unicode": "1F192",
+  "cool": {
+    "category": "symbols",
+    "moji": "🆒",
+    "unicodeVersion": "6.0",
     "digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1"
   },
-  {
-    "name": "cop",
-    "unicode": "1F46E",
+  "cop": {
+    "category": "people",
+    "moji": "👮",
+    "unicodeVersion": "6.0",
     "digest": "78996521bbe231d03ebea355226d8a1515f47cde7b2fbeca1037e7b7e5133466"
   },
-  {
-    "name": "cop_tone1",
-    "unicode": "1F46E-1F3FB",
+  "cop_tone1": {
+    "category": "people",
+    "moji": "👮🏻",
+    "unicodeVersion": "8.0",
     "digest": "8a38cd107f5f4c0b821ac43f32df5dc57facaf39fbafb98483ec00fd7df41baf"
   },
-  {
-    "name": "cop_tone2",
-    "unicode": "1F46E-1F3FC",
+  "cop_tone2": {
+    "category": "people",
+    "moji": "👮🏼",
+    "unicodeVersion": "8.0",
     "digest": "8ab8ab086f3ff82aa4bf4760c3c822846ec2696c41d21dffdac12d5afbe398b7"
   },
-  {
-    "name": "cop_tone3",
-    "unicode": "1F46E-1F3FD",
+  "cop_tone3": {
+    "category": "people",
+    "moji": "👮🏽",
+    "unicodeVersion": "8.0",
     "digest": "fce710a99fd44a7c8af3ea01b2007e46d3ff38d7a0dff1ef26d6f893ede7e6d2"
   },
-  {
-    "name": "cop_tone4",
-    "unicode": "1F46E-1F3FE",
+  "cop_tone4": {
+    "category": "people",
+    "moji": "👮🏾",
+    "unicodeVersion": "8.0",
     "digest": "3017dd73ef475379911c5e6c79bd0f9f533dbbc5057bce6a11244faa12996ba0"
   },
-  {
-    "name": "cop_tone5",
-    "unicode": "1F46E-1F3FF",
+  "cop_tone5": {
+    "category": "people",
+    "moji": "👮🏿",
+    "unicodeVersion": "8.0",
     "digest": "a3b8807b3f2a8d6ee9bcec0339355bda486e8c930f727139f5447a4b046a6307"
   },
-  {
-    "name": "copyright",
-    "unicode": "00A9",
+  "copyright": {
+    "category": "symbols",
+    "moji": "©",
+    "unicodeVersion": "1.1",
     "digest": "cc28663cdd3f8333d9bb57b511348cde4e51bda19cf0629dccb05c8fc425e079"
   },
-  {
-    "name": "corn",
-    "unicode": "1F33D",
+  "corn": {
+    "category": "food",
+    "moji": "🌽",
+    "unicodeVersion": "6.0",
     "digest": "a099a0b291fa758690e6ee6c762b9ade9a0e3350a707c52d968dfffbcc467de5"
   },
-  {
-    "name": "couch",
-    "unicode": "1F6CB",
-    "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
-  },
-  {
-    "name": "couch_and_lamp",
-    "unicode": "1F6CB",
+  "couch": {
+    "category": "objects",
+    "moji": "🛋",
+    "unicodeVersion": "7.0",
     "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
   },
-  {
-    "name": "couple",
-    "unicode": "1F46B",
+  "couple": {
+    "category": "people",
+    "moji": "👫",
+    "unicodeVersion": "6.0",
     "digest": "c897ba76e24e2f43a4aa261c2754800a8473f43c7ce53f9909a6af2c4897732a"
   },
-  {
-    "name": "couple_mm",
-    "unicode": "1F468-2764-1F468",
-    "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
-  },
-  {
-    "name": "couple_with_heart_mm",
-    "unicode": "1F468-2764-1F468",
+  "couple_mm": {
+    "category": "people",
+    "moji": "👨‍❤️‍👨",
+    "unicodeVersion": "6.0",
     "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
   },
-  {
-    "name": "couple_with_heart",
-    "unicode": "1F491",
+  "couple_with_heart": {
+    "category": "people",
+    "moji": "💑",
+    "unicodeVersion": "6.0",
     "digest": "420bfa81bad10365550c77a98e1c07eb00d03663fe7b610fab1aca8a0a9d201b"
   },
-  {
-    "name": "couple_ww",
-    "unicode": "1F469-2764-1F469",
-    "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
-  },
-  {
-    "name": "couple_with_heart_ww",
-    "unicode": "1F469-2764-1F469",
+  "couple_ww": {
+    "category": "people",
+    "moji": "👩‍❤️‍👩",
+    "unicodeVersion": "6.0",
     "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
   },
-  {
-    "name": "couplekiss",
-    "unicode": "1F48F",
+  "couplekiss": {
+    "category": "people",
+    "moji": "💏",
+    "unicodeVersion": "6.0",
     "digest": "1acfef9d375c4c1deb235babd856b0f90ad4f3194751694cb6abb44f00f29e42"
   },
-  {
-    "name": "cow",
-    "unicode": "1F42E",
+  "cow": {
+    "category": "nature",
+    "moji": "🐮",
+    "unicodeVersion": "6.0",
     "digest": "d71c854ff8b343ee24b8c2b9d56c7cb3fc6fa1a6dc0d7a137841b9f646e6d71b"
   },
-  {
-    "name": "cow2",
-    "unicode": "1F404",
+  "cow2": {
+    "category": "nature",
+    "moji": "🐄",
+    "unicodeVersion": "6.0",
     "digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339"
   },
-  {
-    "name": "cowboy",
-    "unicode": "1F920",
-    "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
-  },
-  {
-    "name": "face_with_cowboy_hat",
-    "unicode": "1F920",
+  "cowboy": {
+    "category": "people",
+    "moji": "🤠",
+    "unicodeVersion": "9.0",
     "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
   },
-  {
-    "name": "crab",
-    "unicode": "1F980",
+  "crab": {
+    "category": "nature",
+    "moji": "🦀",
+    "unicodeVersion": "8.0",
     "digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5"
   },
-  {
-    "name": "crayon",
-    "unicode": "1F58D",
-    "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
-  },
-  {
-    "name": "lower_left_crayon",
-    "unicode": "1F58D",
+  "crayon": {
+    "category": "objects",
+    "moji": "🖍",
+    "unicodeVersion": "7.0",
     "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
   },
-  {
-    "name": "credit_card",
-    "unicode": "1F4B3",
+  "credit_card": {
+    "category": "objects",
+    "moji": "💳",
+    "unicodeVersion": "6.0",
     "digest": "808cd120fd3738eb2be1f6c6c029d98387b0e03fca7d1451e8fbf9c5ab3f643f"
   },
-  {
-    "name": "crescent_moon",
-    "unicode": "1F319",
+  "crescent_moon": {
+    "category": "nature",
+    "moji": "🌙",
+    "unicodeVersion": "6.0",
     "digest": "042e7e01e6e88b97a763b7cc41e2a2b3fe68a649bacf4a090cd28fc653baf640"
   },
-  {
-    "name": "cricket",
-    "unicode": "1F3CF",
-    "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
-  },
-  {
-    "name": "cricket_bat_ball",
-    "unicode": "1F3CF",
+  "cricket": {
+    "category": "activity",
+    "moji": "🏏",
+    "unicodeVersion": "8.0",
     "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
   },
-  {
-    "name": "crocodile",
-    "unicode": "1F40A",
+  "crocodile": {
+    "category": "nature",
+    "moji": "🐊",
+    "unicodeVersion": "6.0",
     "digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992"
   },
-  {
-    "name": "croissant",
-    "unicode": "1F950",
+  "croissant": {
+    "category": "food",
+    "moji": "🥐",
+    "unicodeVersion": "9.0",
     "digest": "b751e287157a1e276617a841a5b5f7f1208ca226cfd8fa947f144390b65a5e16"
   },
-  {
-    "name": "cross",
-    "unicode": "271D",
-    "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
-  },
-  {
-    "name": "latin_cross",
-    "unicode": "271D",
+  "cross": {
+    "category": "symbols",
+    "moji": "✝",
+    "unicodeVersion": "1.1",
     "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
   },
-  {
-    "name": "crossed_flags",
-    "unicode": "1F38C",
+  "crossed_flags": {
+    "category": "objects",
+    "moji": "🎌",
+    "unicodeVersion": "6.0",
     "digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262"
   },
-  {
-    "name": "crossed_swords",
-    "unicode": "2694",
+  "crossed_swords": {
+    "category": "objects",
+    "moji": "⚔",
+    "unicodeVersion": "4.1",
     "digest": "3771a5b26b514236521ce44e15f7730fa9148c6a782b9b600ab870a1f7de6f9f"
   },
-  {
-    "name": "crown",
-    "unicode": "1F451",
+  "crown": {
+    "category": "people",
+    "moji": "👑",
+    "unicodeVersion": "6.0",
     "digest": "6741e58d8f823194e0a3484ac1563e20d9e0b44c1bc46d82444dfffa092cdfc7"
   },
-  {
-    "name": "cruise_ship",
-    "unicode": "1F6F3",
-    "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
-  },
-  {
-    "name": "passenger_ship",
-    "unicode": "1F6F3",
+  "cruise_ship": {
+    "category": "travel",
+    "moji": "🛳",
+    "unicodeVersion": "7.0",
     "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
   },
-  {
-    "name": "cry",
-    "unicode": "1F622",
+  "cry": {
+    "category": "people",
+    "moji": "😢",
+    "unicodeVersion": "6.0",
     "digest": "fc3307ec4fe75539770c1123a0e8e721d9e021009a502655132f68d7cc453816"
   },
-  {
-    "name": "crying_cat_face",
-    "unicode": "1F63F",
+  "crying_cat_face": {
+    "category": "people",
+    "moji": "😿",
+    "unicodeVersion": "6.0",
     "digest": "4942c24935c22babdcb8af41d2c0a7588356b6b674bc238902e2f10ad03e2c5b"
   },
-  {
-    "name": "crystal_ball",
-    "unicode": "1F52E",
+  "crystal_ball": {
+    "category": "objects",
+    "moji": "🔮",
+    "unicodeVersion": "6.0",
     "digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30"
   },
-  {
-    "name": "cucumber",
-    "unicode": "1F952",
+  "cucumber": {
+    "category": "food",
+    "moji": "🥒",
+    "unicodeVersion": "9.0",
     "digest": "d1196e23f2f155ef5c1330f8497f40957a7357cb177127f457c5c471f0a23727"
   },
-  {
-    "name": "cupid",
-    "unicode": "1F498",
+  "cupid": {
+    "category": "symbols",
+    "moji": "💘",
+    "unicodeVersion": "6.0",
     "digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658"
   },
-  {
-    "name": "curly_loop",
-    "unicode": "27B0",
+  "curly_loop": {
+    "category": "symbols",
+    "moji": "➰",
+    "unicodeVersion": "6.0",
     "digest": "9e4eb98d6597888f91208080c6a79824adb432ea34f46c85da26cb630bd1cc73"
   },
-  {
-    "name": "currency_exchange",
-    "unicode": "1F4B1",
+  "currency_exchange": {
+    "category": "symbols",
+    "moji": "💱",
+    "unicodeVersion": "6.0",
     "digest": "b85377265b9876888969aa42b65bba0be523a370175baf226f20131e535af554"
   },
-  {
-    "name": "curry",
-    "unicode": "1F35B",
+  "curry": {
+    "category": "food",
+    "moji": "🍛",
+    "unicodeVersion": "6.0",
     "digest": "a01c0a713662817720b485f7739f57e61afc025f5c43792f4de961c94f92f31e"
   },
-  {
-    "name": "custard",
-    "unicode": "1F36E",
+  "custard": {
+    "category": "food",
+    "moji": "🍮",
+    "unicodeVersion": "6.0",
     "digest": "85c2b9ac904134a6c3587eb0a0806f2ab4282c5ed5c79d41734f3203998f757e"
   },
-  {
-    "name": "customs",
-    "unicode": "1F6C3",
+  "customs": {
+    "category": "symbols",
+    "moji": "🛃",
+    "unicodeVersion": "6.0",
     "digest": "eb2546e1e617d4c1a1f614318af5e5dacf3e8d9479ffa08108977defa83ded32"
   },
-  {
-    "name": "cyclone",
-    "unicode": "1F300",
+  "cyclone": {
+    "category": "symbols",
+    "moji": "🌀",
+    "unicodeVersion": "6.0",
     "digest": "7a0f8564d76adf2d0ed272f56dc0d01fb7b557852e0ca797e73f5472b8630bf3"
   },
-  {
-    "name": "dagger",
-    "unicode": "1F5E1",
-    "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
-  },
-  {
-    "name": "dagger_knife",
-    "unicode": "1F5E1",
+  "dagger": {
+    "category": "objects",
+    "moji": "🗡",
+    "unicodeVersion": "7.0",
     "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
   },
-  {
-    "name": "dancer",
-    "unicode": "1F483",
+  "dancer": {
+    "category": "people",
+    "moji": "💃",
+    "unicodeVersion": "6.0",
     "digest": "66ffa86827e85acae4aa870c0859fe3a9dad03d21ff4bc800b61c95c902a8a90"
   },
-  {
-    "name": "dancer_tone1",
-    "unicode": "1F483-1F3FB",
+  "dancer_tone1": {
+    "category": "people",
+    "moji": "💃🏻",
+    "unicodeVersion": "8.0",
     "digest": "bdbee740addc890e369d3469a3585eb0d1e4fbc7e04dd6f6aca762d8aeee6a8c"
   },
-  {
-    "name": "dancer_tone2",
-    "unicode": "1F483-1F3FC",
+  "dancer_tone2": {
+    "category": "people",
+    "moji": "💃🏼",
+    "unicodeVersion": "8.0",
     "digest": "9f7b4c627241eaa2def9717a5286a423f0b9c1b044dd9ea4442a76f1858d14a4"
   },
-  {
-    "name": "dancer_tone3",
-    "unicode": "1F483-1F3FD",
+  "dancer_tone3": {
+    "category": "people",
+    "moji": "💃🏽",
+    "unicodeVersion": "8.0",
     "digest": "a6bd49a377ce6c2004bf126b6f66d0b21d8c14103c2add7b10f12ed9e1c2d302"
   },
-  {
-    "name": "dancer_tone4",
-    "unicode": "1F483-1F3FE",
+  "dancer_tone4": {
+    "category": "people",
+    "moji": "💃🏾",
+    "unicodeVersion": "8.0",
     "digest": "4ec2a7629c01b0e9006b5cda4deae3bf297ce3b71d18063f93eeb5c14be19a1a"
   },
-  {
-    "name": "dancer_tone5",
-    "unicode": "1F483-1F3FF",
+  "dancer_tone5": {
+    "category": "people",
+    "moji": "💃🏿",
+    "unicodeVersion": "8.0",
     "digest": "2b48e3a6b366c6f55f73b816e6fb03c39e9890f586f7e9c9043cf0c013d9cdd5"
   },
-  {
-    "name": "dancers",
-    "unicode": "1F46F",
+  "dancers": {
+    "category": "people",
+    "moji": "👯",
+    "unicodeVersion": "6.0",
     "digest": "12be66ed19d232bb387270f40bece68bd0cb2342b318f6c9bb8b49c64ff7d0ad"
   },
-  {
-    "name": "dango",
-    "unicode": "1F361",
+  "dango": {
+    "category": "food",
+    "moji": "🍡",
+    "unicodeVersion": "6.0",
     "digest": "34e8cd153c50f2d725abe8934c35c96a3ab533f0cc5fbb1e1474eafad1dc1fc2"
   },
-  {
-    "name": "dark_sunglasses",
-    "unicode": "1F576",
+  "dark_sunglasses": {
+    "category": "people",
+    "moji": "🕶",
+    "unicodeVersion": "7.0",
     "digest": "d0a735ad5bf0ece00af2a21abf950b89292ebd8ca6e28b1dbb1368252fb44afe"
   },
-  {
-    "name": "dart",
-    "unicode": "1F3AF",
+  "dart": {
+    "category": "activity",
+    "moji": "🎯",
+    "unicodeVersion": "6.0",
     "digest": "998642f06a875905e0a6bf30963c025baff1cf55b8e76884b9119f2d71188b0c"
   },
-  {
-    "name": "dash",
-    "unicode": "1F4A8",
+  "dash": {
+    "category": "nature",
+    "moji": "💨",
+    "unicodeVersion": "6.0",
     "digest": "f7aae7d3887c67d76f3329c2dc9e6807dc580a4b07ab35599c7805e41823a345"
   },
-  {
-    "name": "date",
-    "unicode": "1F4C5",
+  "date": {
+    "category": "objects",
+    "moji": "📅",
+    "unicodeVersion": "6.0",
     "digest": "d0b695e4a7cfbbe71b4fbebf345b66ca98f0cf1c751362928e54c23ca78d4c7b"
   },
-  {
-    "name": "deciduous_tree",
-    "unicode": "1F333",
+  "deciduous_tree": {
+    "category": "nature",
+    "moji": "🌳",
+    "unicodeVersion": "6.0",
     "digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790"
   },
-  {
-    "name": "deer",
-    "unicode": "1F98C",
+  "deer": {
+    "category": "nature",
+    "moji": "🦌",
+    "unicodeVersion": "9.0",
     "digest": "7f4302ca68fd121ee73be48d0a0a0fb9e7e2741071a491ad2b7b0eab9f11ad25"
   },
-  {
-    "name": "department_store",
-    "unicode": "1F3EC",
+  "department_store": {
+    "category": "travel",
+    "moji": "🏬",
+    "unicodeVersion": "6.0",
     "digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b"
   },
-  {
-    "name": "desert",
-    "unicode": "1F3DC",
+  "desert": {
+    "category": "travel",
+    "moji": "🏜",
+    "unicodeVersion": "7.0",
     "digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e"
   },
-  {
-    "name": "desktop",
-    "unicode": "1F5A5",
-    "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
-  },
-  {
-    "name": "desktop_computer",
-    "unicode": "1F5A5",
+  "desktop": {
+    "category": "objects",
+    "moji": "🖥",
+    "unicodeVersion": "7.0",
     "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
   },
-  {
-    "name": "diamond_shape_with_a_dot_inside",
-    "unicode": "1F4A0",
+  "diamond_shape_with_a_dot_inside": {
+    "category": "symbols",
+    "moji": "💠",
+    "unicodeVersion": "6.0",
     "digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3"
   },
-  {
-    "name": "diamonds",
-    "unicode": "2666",
+  "diamonds": {
+    "category": "symbols",
+    "moji": "♦",
+    "unicodeVersion": "1.1",
     "digest": "bf3d9a020afe8aa226db73590bc193a9c2c3e6e642edd2445c5960c3e67cf153"
   },
-  {
-    "name": "disappointed",
-    "unicode": "1F61E",
+  "disappointed": {
+    "category": "people",
+    "moji": "😞",
+    "unicodeVersion": "6.0",
     "digest": "c0f406c6beea0fd1328adefc097d04aa16b72f7a5afa0867967d8ea25d72db17"
   },
-  {
-    "name": "disappointed_relieved",
-    "unicode": "1F625",
+  "disappointed_relieved": {
+    "category": "people",
+    "moji": "😥",
+    "unicodeVersion": "6.0",
     "digest": "c826f5dd4f2f7e5289d720851d4826ab8284d915606c1b152ab229b7fadbba14"
   },
-  {
-    "name": "dividers",
-    "unicode": "1F5C2",
-    "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
-  },
-  {
-    "name": "card_index_dividers",
-    "unicode": "1F5C2",
+  "dividers": {
+    "category": "objects",
+    "moji": "🗂",
+    "unicodeVersion": "7.0",
     "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
   },
-  {
-    "name": "dizzy",
-    "unicode": "1F4AB",
+  "dizzy": {
+    "category": "nature",
+    "moji": "💫",
+    "unicodeVersion": "6.0",
     "digest": "d577545c2de42389695447c6ebbfef895f30f0fda84eef45684f9bf4a9c27ff1"
   },
-  {
-    "name": "dizzy_face",
-    "unicode": "1F635",
+  "dizzy_face": {
+    "category": "people",
+    "moji": "😵",
+    "unicodeVersion": "6.0",
     "digest": "7b3aeaffb4e15ccf633b91dda4a44847a1eb28d78ce58b4d171b20a771bde414"
   },
-  {
-    "name": "do_not_litter",
-    "unicode": "1F6AF",
+  "do_not_litter": {
+    "category": "symbols",
+    "moji": "🚯",
+    "unicodeVersion": "6.0",
     "digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb"
   },
-  {
-    "name": "dog",
-    "unicode": "1F436",
+  "dog": {
+    "category": "nature",
+    "moji": "🐶",
+    "unicodeVersion": "6.0",
     "digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11"
   },
-  {
-    "name": "dog2",
-    "unicode": "1F415",
+  "dog2": {
+    "category": "nature",
+    "moji": "🐕",
+    "unicodeVersion": "6.0",
     "digest": "0a8901bce5ed994533ff84299b2a1364de28d872c9f9510d3426a83e8a9d2e34"
   },
-  {
-    "name": "dollar",
-    "unicode": "1F4B5",
+  "dollar": {
+    "category": "objects",
+    "moji": "💵",
+    "unicodeVersion": "6.0",
     "digest": "52438e38867aedc021740bb41f9ba336e75a50faa148419412a01d75d8c93155"
   },
-  {
-    "name": "dolls",
-    "unicode": "1F38E",
+  "dolls": {
+    "category": "objects",
+    "moji": "🎎",
+    "unicodeVersion": "6.0",
     "digest": "a687184e9a0915deef44bb3cacfb19d3f3f19cf2c110f1da90191dd567333c57"
   },
-  {
-    "name": "dolphin",
-    "unicode": "1F42C",
+  "dolphin": {
+    "category": "nature",
+    "moji": "🐬",
+    "unicodeVersion": "6.0",
     "digest": "0b7ee08f4236232ca533ed3a3023d28020d36f178efaec5ce8b0e13a84778512"
   },
-  {
-    "name": "door",
-    "unicode": "1F6AA",
+  "door": {
+    "category": "objects",
+    "moji": "🚪",
+    "unicodeVersion": "6.0",
     "digest": "984a9ca88852ebdb539e0c385d9c6ffe5010e9189bc372a3d00f5c8d44c8e6f5"
   },
-  {
-    "name": "doughnut",
-    "unicode": "1F369",
+  "doughnut": {
+    "category": "food",
+    "moji": "🍩",
+    "unicodeVersion": "6.0",
     "digest": "27634587e6a53807baa32157bb06b0e115c8ad8aefebba7ebb0b65a084170e3a"
   },
-  {
-    "name": "dove",
-    "unicode": "1F54A",
-    "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
-  },
-  {
-    "name": "dove_of_peace",
-    "unicode": "1F54A",
+  "dove": {
+    "category": "nature",
+    "moji": "🕊",
+    "unicodeVersion": "7.0",
     "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
   },
-  {
-    "name": "dragon",
-    "unicode": "1F409",
+  "dragon": {
+    "category": "nature",
+    "moji": "🐉",
+    "unicodeVersion": "6.0",
     "digest": "2abcb3d945d848e34ffc76203b29ef26df7458856166fffd155611f7bbe72652"
   },
-  {
-    "name": "dragon_face",
-    "unicode": "1F432",
+  "dragon_face": {
+    "category": "nature",
+    "moji": "🐲",
+    "unicodeVersion": "6.0",
     "digest": "0030548931b931e3b51f26cf660394aee36499e688ba83ce9cfccb635dcd4d54"
   },
-  {
-    "name": "dress",
-    "unicode": "1F457",
+  "dress": {
+    "category": "people",
+    "moji": "👗",
+    "unicodeVersion": "6.0",
     "digest": "96ceba928fb356f7c0ae99bf22552321f08a65d5f1c0340ab89641219ad366ad"
   },
-  {
-    "name": "dromedary_camel",
-    "unicode": "1F42A",
+  "dromedary_camel": {
+    "category": "nature",
+    "moji": "🐪",
+    "unicodeVersion": "6.0",
     "digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d"
   },
-  {
-    "name": "drooling_face",
-    "unicode": "1F924",
-    "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
-  },
-  {
-    "name": "drool",
-    "unicode": "1F924",
+  "drooling_face": {
+    "category": "people",
+    "moji": "🤤",
+    "unicodeVersion": "9.0",
     "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
   },
-  {
-    "name": "droplet",
-    "unicode": "1F4A7",
+  "droplet": {
+    "category": "nature",
+    "moji": "💧",
+    "unicodeVersion": "6.0",
     "digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3"
   },
-  {
-    "name": "drum",
-    "unicode": "1F941",
-    "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
-  },
-  {
-    "name": "drum_with_drumsticks",
-    "unicode": "1F941",
+  "drum": {
+    "category": "activity",
+    "moji": "🥁",
+    "unicodeVersion": "9.0",
     "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
   },
-  {
-    "name": "duck",
-    "unicode": "1F986",
+  "duck": {
+    "category": "nature",
+    "moji": "🦆",
+    "unicodeVersion": "9.0",
     "digest": "8f8373798a7727368b32328e7a9a349727a949e7391ddd243b6456141a4f7e94"
   },
-  {
-    "name": "dvd",
-    "unicode": "1F4C0",
+  "dvd": {
+    "category": "objects",
+    "moji": "📀",
+    "unicodeVersion": "6.0",
     "digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f"
   },
-  {
-    "name": "e-mail",
-    "unicode": "1F4E7",
-    "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
-  },
-  {
-    "name": "email",
-    "unicode": "1F4E7",
+  "e-mail": {
+    "category": "objects",
+    "moji": "📧",
+    "unicodeVersion": "6.0",
     "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
   },
-  {
-    "name": "eagle",
-    "unicode": "1F985",
+  "eagle": {
+    "category": "nature",
+    "moji": "🦅",
+    "unicodeVersion": "9.0",
     "digest": "b44fd4f61b83c5114358a272343ac9b0eabbc70847f739bbdbf8aae3ade5bc1d"
   },
-  {
-    "name": "ear",
-    "unicode": "1F442",
+  "ear": {
+    "category": "people",
+    "moji": "👂",
+    "unicodeVersion": "6.0",
     "digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8"
   },
-  {
-    "name": "ear_of_rice",
-    "unicode": "1F33E",
+  "ear_of_rice": {
+    "category": "nature",
+    "moji": "🌾",
+    "unicodeVersion": "6.0",
     "digest": "2997c340c2b333d6ba9b73f94ff1a1881735fe0cc4f0c72d7719b305499fc425"
   },
-  {
-    "name": "ear_tone1",
-    "unicode": "1F442-1F3FB",
+  "ear_tone1": {
+    "category": "people",
+    "moji": "👂🏻",
+    "unicodeVersion": "8.0",
     "digest": "5ca759b8569a377a4e63e30d94b585b9f76d15348a8a0c1ba19fdc522790615e"
   },
-  {
-    "name": "ear_tone2",
-    "unicode": "1F442-1F3FC",
+  "ear_tone2": {
+    "category": "people",
+    "moji": "👂🏼",
+    "unicodeVersion": "8.0",
     "digest": "12aafb3ef2cfcdc892b2877c2e24920620f0f77f850e12afbfe55eadce9e37df"
   },
-  {
-    "name": "ear_tone3",
-    "unicode": "1F442-1F3FD",
+  "ear_tone3": {
+    "category": "people",
+    "moji": "👂🏽",
+    "unicodeVersion": "8.0",
     "digest": "f4d28d9f72cf116ac92d80061eb84c918d6523bf53b2ad526f5457aba487d527"
   },
-  {
-    "name": "ear_tone4",
-    "unicode": "1F442-1F3FE",
+  "ear_tone4": {
+    "category": "people",
+    "moji": "👂🏾",
+    "unicodeVersion": "8.0",
     "digest": "eaa9453670f7e3adc6ec6934ee70efc9bf60fe6c99c5804b7ba9e3804aec65de"
   },
-  {
-    "name": "ear_tone5",
-    "unicode": "1F442-1F3FF",
+  "ear_tone5": {
+    "category": "people",
+    "moji": "👂🏿",
+    "unicodeVersion": "8.0",
     "digest": "54bd0782419489556b80e9e0d15b05df74757aa4e04ba565f45c20d3dd60e3f1"
   },
-  {
-    "name": "earth_africa",
-    "unicode": "1F30D",
+  "earth_africa": {
+    "category": "nature",
+    "moji": "🌍",
+    "unicodeVersion": "6.0",
     "digest": "c691a6f591f5a07b268fd64efe113e81cec8d5963ad83ced2537422343ff7ecf"
   },
-  {
-    "name": "earth_americas",
-    "unicode": "1F30E",
+  "earth_americas": {
+    "category": "nature",
+    "moji": "🌎",
+    "unicodeVersion": "6.0",
     "digest": "a9c60cf8341ff59a9cc1a715b7144af734fcd28915a8e003a31ebf2abf9aedb1"
   },
-  {
-    "name": "earth_asia",
-    "unicode": "1F30F",
+  "earth_asia": {
+    "category": "nature",
+    "moji": "🌏",
+    "unicodeVersion": "6.0",
     "digest": "ee2beb61fb8c87279161c5a8c4ad17bb71ce790123f8fa33522941d027e060a5"
   },
-  {
-    "name": "egg",
-    "unicode": "1F95A",
+  "egg": {
+    "category": "food",
+    "moji": "🥚",
+    "unicodeVersion": "9.0",
     "digest": "72b9c841af784e7cbccbbe48ba833df5cecdd284397c199cab079872e879d92f"
   },
-  {
-    "name": "eggplant",
-    "unicode": "1F346",
+  "eggplant": {
+    "category": "food",
+    "moji": "🍆",
+    "unicodeVersion": "6.0",
     "digest": "ec0a460e0cf0e615f51279677594a899672e1b4ecd9396e17a8cfa2a3efe5238"
   },
-  {
-    "name": "eight",
-    "unicode": "0038-20E3",
+  "eight": {
+    "category": "symbols",
+    "moji": "8️⃣",
+    "unicodeVersion": "3.0",
     "digest": "57ff905033a32747690adba6486d12b09eb4d45de556f4e1ab6fb04e1fb861a8"
   },
-  {
-    "name": "eight_pointed_black_star",
-    "unicode": "2734",
+  "eight_pointed_black_star": {
+    "category": "symbols",
+    "moji": "✴",
+    "unicodeVersion": "1.1",
     "digest": "7bf11f6e28591e3d0625296aaabf4ecb75c982e425abf3049339e93494acc17e"
   },
-  {
-    "name": "eight_spoked_asterisk",
-    "unicode": "2733",
+  "eight_spoked_asterisk": {
+    "category": "symbols",
+    "moji": "✳",
+    "unicodeVersion": "1.1",
     "digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26"
   },
-  {
-    "name": "eject",
-    "unicode": "23CF",
-    "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
-  },
-  {
-    "name": "eject_symbol",
-    "unicode": "23CF",
+  "eject": {
+    "category": "symbols",
+    "moji": "⏏",
+    "unicodeVersion": "4.0",
     "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
   },
-  {
-    "name": "electric_plug",
-    "unicode": "1F50C",
+  "electric_plug": {
+    "category": "objects",
+    "moji": "🔌",
+    "unicodeVersion": "6.0",
     "digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0"
   },
-  {
-    "name": "elephant",
-    "unicode": "1F418",
+  "elephant": {
+    "category": "nature",
+    "moji": "🐘",
+    "unicodeVersion": "6.0",
     "digest": "b7750f4b013fbd28ac5330e1694ef4d3b4a9c6fc7b807879db0c24b035a16c29"
   },
-  {
-    "name": "end",
-    "unicode": "1F51A",
+  "end": {
+    "category": "symbols",
+    "moji": "🔚",
+    "unicodeVersion": "6.0",
     "digest": "dd93aee6986eb637a8b58f234da47568b88525599f73246e322af030351997a2"
   },
-  {
-    "name": "envelope",
-    "unicode": "2709",
+  "envelope": {
+    "category": "objects",
+    "moji": "✉",
+    "unicodeVersion": "1.1",
     "digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78"
   },
-  {
-    "name": "envelope_with_arrow",
-    "unicode": "1F4E9",
+  "envelope_with_arrow": {
+    "category": "objects",
+    "moji": "📩",
+    "unicodeVersion": "6.0",
     "digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6"
   },
-  {
-    "name": "euro",
-    "unicode": "1F4B6",
+  "euro": {
+    "category": "objects",
+    "moji": "💶",
+    "unicodeVersion": "6.0",
     "digest": "3af3e223e8f26468a94f6f5c17198432656e8d20b3bab31566c2b5a86e717df4"
   },
-  {
-    "name": "european_castle",
-    "unicode": "1F3F0",
+  "european_castle": {
+    "category": "travel",
+    "moji": "🏰",
+    "unicodeVersion": "6.0",
     "digest": "21082d0be7e3b2794e59ff0170da0cfe42a9b734cf02704603e3b52ff48202ba"
   },
-  {
-    "name": "european_post_office",
-    "unicode": "1F3E4",
+  "european_post_office": {
+    "category": "travel",
+    "moji": "🏤",
+    "unicodeVersion": "6.0",
     "digest": "02b4c7602939f0cb9cb2b4e05996bcdb6bd93cf8025c2ea02db8cbe13ca397d0"
   },
-  {
-    "name": "evergreen_tree",
-    "unicode": "1F332",
+  "evergreen_tree": {
+    "category": "nature",
+    "moji": "🌲",
+    "unicodeVersion": "6.0",
     "digest": "74b226098e66c0a94a92e0f22b9d631736e12dca72c34182c9d0ba56aa593172"
   },
-  {
-    "name": "exclamation",
-    "unicode": "2757",
+  "exclamation": {
+    "category": "symbols",
+    "moji": "❗",
+    "unicodeVersion": "5.2",
     "digest": "45b87ae4593656d7da49ff5645fb6a2a18d582553295358da9f09f1ae8272445"
   },
-  {
-    "name": "expressionless",
-    "unicode": "1F611",
+  "expressionless": {
+    "category": "people",
+    "moji": "😑",
+    "unicodeVersion": "6.1",
     "digest": "34e2a1c8121f4f0bc4ce33d226d8cc1a4ebf5260746df2b23e29eef24ee9372e"
   },
-  {
-    "name": "eye",
-    "unicode": "1F441",
+  "eye": {
+    "category": "people",
+    "moji": "👁",
+    "unicodeVersion": "7.0",
     "digest": "79ecff79c2edee630e72725b54e67ee2e96d24ca03fef2954a56a09c0a2227f8"
   },
-  {
-    "name": "eye_in_speech_bubble",
-    "unicode": "1F441-1F5E8",
+  "eye_in_speech_bubble": {
+    "category": "symbols",
+    "moji": "👁‍🗨",
+    "unicodeVersion": "7.0",
     "digest": "c0050c026c2a3060723cab2df2603c1c7da7ed81faedb9ebe16cd89721928a55"
   },
-  {
-    "name": "eyeglasses",
-    "unicode": "1F453",
+  "eyeglasses": {
+    "category": "people",
+    "moji": "👓",
+    "unicodeVersion": "6.0",
     "digest": "d4a9585d6c43ef514a97c45c64607162e775a45544821f1470c6f8f25b93ab81"
   },
-  {
-    "name": "eyes",
-    "unicode": "1F440",
+  "eyes": {
+    "category": "people",
+    "moji": "👀",
+    "unicodeVersion": "6.0",
     "digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8"
   },
-  {
-    "name": "face_palm",
-    "unicode": "1F926",
+  "face_palm": {
+    "category": "people",
+    "moji": "🤦",
+    "unicodeVersion": "9.0",
     "digest": "4ec873048b34b1bb34430724cf28e4bee6c0a9eee88ce39b9d1565047dc92420"
   },
-  {
-    "name": "face_palm_tone1",
-    "unicode": "1F926-1F3FB",
+  "face_palm_tone1": {
+    "category": "people",
+    "moji": "🤦🏻",
+    "unicodeVersion": "9.0",
     "digest": "e93ef92b4c01dbea6c400e708e23dd36da92ccfbf5eb4f177b3b20c3a46bdc19"
   },
-  {
-    "name": "face_palm_tone2",
-    "unicode": "1F926-1F3FC",
+  "face_palm_tone2": {
+    "category": "people",
+    "moji": "🤦🏼",
+    "unicodeVersion": "9.0",
     "digest": "22c8bf9fd9fa2ed9dca7a6397ed00ba6cfe9aeef2b0fb7b516ee4dda0df050ea"
   },
-  {
-    "name": "face_palm_tone3",
-    "unicode": "1F926-1F3FD",
+  "face_palm_tone3": {
+    "category": "people",
+    "moji": "🤦🏽",
+    "unicodeVersion": "9.0",
     "digest": "c0b8bb9d2423e6787b6bdf1ca5a13f52853e4f48a9a1af0f2d4af1364fff022e"
   },
-  {
-    "name": "face_palm_tone4",
-    "unicode": "1F926-1F3FE",
+  "face_palm_tone4": {
+    "category": "people",
+    "moji": "🤦🏾",
+    "unicodeVersion": "9.0",
     "digest": "f522ab186adcbb4549ea2c03500cdd7a86add548e43ebf7a54d58cc24deea072"
   },
-  {
-    "name": "face_palm_tone5",
-    "unicode": "1F926-1F3FF",
+  "face_palm_tone5": {
+    "category": "people",
+    "moji": "🤦🏿",
+    "unicodeVersion": "9.0",
     "digest": "363507ae7178b5ec583635f47bcab10c897346f48b85d8759b1004c32cd8ad65"
   },
-  {
-    "name": "factory",
-    "unicode": "1F3ED",
+  "factory": {
+    "category": "travel",
+    "moji": "🏭",
+    "unicodeVersion": "6.0",
     "digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0"
   },
-  {
-    "name": "fallen_leaf",
-    "unicode": "1F342",
+  "fallen_leaf": {
+    "category": "nature",
+    "moji": "🍂",
+    "unicodeVersion": "6.0",
     "digest": "81fce04231d48db0e55f3697f930e9a7e3306bed5e35f1234e98c40a24ac5626"
   },
-  {
-    "name": "family",
-    "unicode": "1F46A",
+  "family": {
+    "category": "people",
+    "moji": "👪",
+    "unicodeVersion": "6.0",
     "digest": "06f2ce63768ffe43b3d9b2a9660b34d043f37b3c91610dd62343ba21df8ecbe5"
   },
-  {
-    "name": "family_mmb",
-    "unicode": "1F468-1F468-1F466",
+  "family_mmb": {
+    "category": "people",
+    "moji": "👨‍👨‍👦",
+    "unicodeVersion": "6.0",
     "digest": "41a18405be796699a7eb7c36ab6f7d898e322749997f45387377acf5bb16a50f"
   },
-  {
-    "name": "family_mmbb",
-    "unicode": "1F468-1F468-1F466-1F466",
+  "family_mmbb": {
+    "category": "people",
+    "moji": "👨‍👨‍👦‍👦",
+    "unicodeVersion": "6.0",
     "digest": "87255d1d18c6971c8c083c818e598424c1bd717eed892478b7e9516639dbfb45"
   },
-  {
-    "name": "family_mmg",
-    "unicode": "1F468-1F468-1F467",
+  "family_mmg": {
+    "category": "people",
+    "moji": "👨‍👨‍👧",
+    "unicodeVersion": "6.0",
     "digest": "a132b1b8f10b318d8e23aee15dab4caa14528aeb3c89966d4bcc25fb54af72ad"
   },
-  {
-    "name": "family_mmgb",
-    "unicode": "1F468-1F468-1F467-1F466",
+  "family_mmgb": {
+    "category": "people",
+    "moji": "👨‍👨‍👧‍👦",
+    "unicodeVersion": "6.0",
     "digest": "eb2bc1966df406aaf38ce5a58db9324162799cdacf31f74f40e6384807a8efc2"
   },
-  {
-    "name": "family_mmgg",
-    "unicode": "1F468-1F468-1F467-1F467",
+  "family_mmgg": {
+    "category": "people",
+    "moji": "👨‍👨‍👧‍👧",
+    "unicodeVersion": "6.0",
     "digest": "24f3d60f98fbd6b687f7cacfb629390b90509a754036e5439ae5294759c0606b"
   },
-  {
-    "name": "family_mwbb",
-    "unicode": "1F468-1F469-1F466-1F466",
+  "family_mwbb": {
+    "category": "people",
+    "moji": "👨‍👩‍👦‍👦",
+    "unicodeVersion": "6.0",
     "digest": "2f77692bcb9275c4df501b64a18401dcaf8c68b21f26fbdad59b1feab0c98fd1"
   },
-  {
-    "name": "family_mwg",
-    "unicode": "1F468-1F469-1F467",
+  "family_mwg": {
+    "category": "people",
+    "moji": "👨‍👩‍👧",
+    "unicodeVersion": "6.0",
     "digest": "1a976d13127665d9386cebfdb24e5572dc499bda484c0ee05585886edc616130"
   },
-  {
-    "name": "family_mwgb",
-    "unicode": "1F468-1F469-1F467-1F466",
+  "family_mwgb": {
+    "category": "people",
+    "moji": "👨‍👩‍👧‍👦",
+    "unicodeVersion": "6.0",
     "digest": "960ec2cbac13ef208e73644cd36711b83e6c070c36950f834f3669812839b7f8"
   },
-  {
-    "name": "family_mwgg",
-    "unicode": "1F468-1F469-1F467-1F467",
+  "family_mwgg": {
+    "category": "people",
+    "moji": "👨‍👩‍👧‍👧",
+    "unicodeVersion": "6.0",
     "digest": "8353b03dfa5c24aba75a0abdfdac01603f593819d54b4c7f2f88aafb31da0c6a"
   },
-  {
-    "name": "family_wwb",
-    "unicode": "1F469-1F469-1F466",
+  "family_wwb": {
+    "category": "people",
+    "moji": "👩‍👩‍👦",
+    "unicodeVersion": "6.0",
     "digest": "07a5dd397718c553573689f6512f386729c13a12d5dc78be47c06405769cd98a"
   },
-  {
-    "name": "family_wwbb",
-    "unicode": "1F469-1F469-1F466-1F466",
+  "family_wwbb": {
+    "category": "people",
+    "moji": "👩‍👩‍👦‍👦",
+    "unicodeVersion": "6.0",
     "digest": "b627f460f1da0d47b0b662402940b2b77c9538d380d05436dfca4b456c50c939"
   },
-  {
-    "name": "family_wwg",
-    "unicode": "1F469-1F469-1F467",
+  "family_wwg": {
+    "category": "people",
+    "moji": "👩‍👩‍👧",
+    "unicodeVersion": "6.0",
     "digest": "2d6f373bed53f1028f0fbe9caf036465a351f37b9e00fca7d722cc5a1984f251"
   },
-  {
-    "name": "family_wwgb",
-    "unicode": "1F469-1F469-1F467-1F466",
+  "family_wwgb": {
+    "category": "people",
+    "moji": "👩‍👩‍👧‍👦",
+    "unicodeVersion": "6.0",
     "digest": "72be5c85e1621f73d6794edd6e428febdb366b9e4c816f7829897fd1ab34642b"
   },
-  {
-    "name": "family_wwgg",
-    "unicode": "1F469-1F469-1F467-1F467",
+  "family_wwgg": {
+    "category": "people",
+    "moji": "👩‍👩‍👧‍👧",
+    "unicodeVersion": "6.0",
     "digest": "c39e0916069460d2d9741bddf58e76f5d6a09254cba0eeb262345adf8630bc32"
   },
-  {
-    "name": "fast_forward",
-    "unicode": "23E9",
+  "fast_forward": {
+    "category": "symbols",
+    "moji": "⏩",
+    "unicodeVersion": "6.0",
     "digest": "e7d2d8085cfd406c2b096e8dd147dd3722290a5727b1f7df185989526a2335ec"
   },
-  {
-    "name": "fax",
-    "unicode": "1F4E0",
+  "fax": {
+    "category": "objects",
+    "moji": "📠",
+    "unicodeVersion": "6.0",
     "digest": "ff85ffa440c5379c9b138ebe2d7912d6098da3b37a051b80442d5557b7f993b0"
   },
-  {
-    "name": "fearful",
-    "unicode": "1F628",
+  "fearful": {
+    "category": "people",
+    "moji": "😨",
+    "unicodeVersion": "6.0",
     "digest": "b72bdf7d075d5c4e38bbd8512fb45fda2e85c9c8732a47e67575ae9f2ed4c5df"
   },
-  {
-    "name": "feet",
-    "unicode": "1F43E",
+  "feet": {
+    "category": "nature",
+    "moji": "🐾",
+    "unicodeVersion": "6.0",
     "digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016"
   },
-  {
-    "name": "fencer",
-    "unicode": "1F93A",
-    "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
-  },
-  {
-    "name": "fencing",
-    "unicode": "1F93A",
+  "fencer": {
+    "category": "activity",
+    "moji": "🤺",
+    "unicodeVersion": "9.0",
     "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
   },
-  {
-    "name": "ferris_wheel",
-    "unicode": "1F3A1",
+  "ferris_wheel": {
+    "category": "travel",
+    "moji": "🎡",
+    "unicodeVersion": "6.0",
     "digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c"
   },
-  {
-    "name": "ferry",
-    "unicode": "26F4",
+  "ferry": {
+    "category": "travel",
+    "moji": "⛴",
+    "unicodeVersion": "5.2",
     "digest": "5002a72af2e3c4cef9a36ad5987aeed7d99f96bfd13e56f78957315ec7e749a3"
   },
-  {
-    "name": "field_hockey",
-    "unicode": "1F3D1",
+  "field_hockey": {
+    "category": "activity",
+    "moji": "🏑",
+    "unicodeVersion": "8.0",
     "digest": "4ee091d96161ba719ab8fd6f2b03f96d902a6f22cffe0563b930618bb8ac2b67"
   },
-  {
-    "name": "file_cabinet",
-    "unicode": "1F5C4",
+  "file_cabinet": {
+    "category": "objects",
+    "moji": "🗄",
+    "unicodeVersion": "7.0",
     "digest": "92914147bf93e6d64271ff99d217a18a9850a367d08a5f9f458ecf9311a5bbe9"
   },
-  {
-    "name": "file_folder",
-    "unicode": "1F4C1",
+  "file_folder": {
+    "category": "objects",
+    "moji": "📁",
+    "unicodeVersion": "6.0",
     "digest": "62a42a929267cfbfdb795ead381c9657c343458bc5fca95ea8a0ab892c61d4f6"
   },
-  {
-    "name": "film_frames",
-    "unicode": "1F39E",
+  "film_frames": {
+    "category": "objects",
+    "moji": "🎞",
+    "unicodeVersion": "7.0",
     "digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a"
   },
-  {
-    "name": "fingers_crossed",
-    "unicode": "1F91E",
-    "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
-  },
-  {
-    "name": "hand_with_index_and_middle_finger_crossed",
-    "unicode": "1F91E",
+  "fingers_crossed": {
+    "category": "people",
+    "moji": "🤞",
+    "unicodeVersion": "9.0",
     "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
   },
-  {
-    "name": "fingers_crossed_tone1",
-    "unicode": "1F91E-1F3FB",
-    "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
-  },
-  {
-    "name": "hand_with_index_and_middle_fingers_crossed_tone1",
-    "unicode": "1F91E-1F3FB",
+  "fingers_crossed_tone1": {
+    "category": "people",
+    "moji": "🤞🏻",
+    "unicodeVersion": "9.0",
     "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
   },
-  {
-    "name": "fingers_crossed_tone2",
-    "unicode": "1F91E-1F3FC",
-    "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
-  },
-  {
-    "name": "hand_with_index_and_middle_fingers_crossed_tone2",
-    "unicode": "1F91E-1F3FC",
+  "fingers_crossed_tone2": {
+    "category": "people",
+    "moji": "🤞🏼",
+    "unicodeVersion": "9.0",
     "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
   },
-  {
-    "name": "fingers_crossed_tone3",
-    "unicode": "1F91E-1F3FD",
-    "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
-  },
-  {
-    "name": "hand_with_index_and_middle_fingers_crossed_tone3",
-    "unicode": "1F91E-1F3FD",
+  "fingers_crossed_tone3": {
+    "category": "people",
+    "moji": "🤞🏽",
+    "unicodeVersion": "9.0",
     "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
   },
-  {
-    "name": "fingers_crossed_tone4",
-    "unicode": "1F91E-1F3FE",
-    "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
-  },
-  {
-    "name": "hand_with_index_and_middle_fingers_crossed_tone4",
-    "unicode": "1F91E-1F3FE",
+  "fingers_crossed_tone4": {
+    "category": "people",
+    "moji": "🤞🏾",
+    "unicodeVersion": "9.0",
     "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
   },
-  {
-    "name": "fingers_crossed_tone5",
-    "unicode": "1F91E-1F3FF",
-    "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
-  },
-  {
-    "name": "hand_with_index_and_middle_fingers_crossed_tone5",
-    "unicode": "1F91E-1F3FF",
+  "fingers_crossed_tone5": {
+    "category": "people",
+    "moji": "🤞🏿",
+    "unicodeVersion": "9.0",
     "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
   },
-  {
-    "name": "fire",
-    "unicode": "1F525",
-    "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
-  },
-  {
-    "name": "flame",
-    "unicode": "1F525",
+  "fire": {
+    "category": "nature",
+    "moji": "🔥",
+    "unicodeVersion": "6.0",
     "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
   },
-  {
-    "name": "fire_engine",
-    "unicode": "1F692",
+  "fire_engine": {
+    "category": "travel",
+    "moji": "🚒",
+    "unicodeVersion": "6.0",
     "digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20"
   },
-  {
-    "name": "fireworks",
-    "unicode": "1F386",
+  "fireworks": {
+    "category": "travel",
+    "moji": "🎆",
+    "unicodeVersion": "6.0",
     "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65"
   },
-  {
-    "name": "first_place",
-    "unicode": "1F947",
-    "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
-  },
-  {
-    "name": "first_place_medal",
-    "unicode": "1F947",
+  "first_place": {
+    "category": "activity",
+    "moji": "🥇",
+    "unicodeVersion": "9.0",
     "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
   },
-  {
-    "name": "first_quarter_moon",
-    "unicode": "1F313",
+  "first_quarter_moon": {
+    "category": "nature",
+    "moji": "🌓",
+    "unicodeVersion": "6.0",
     "digest": "a207ce93084448622a4a5c49c85c566a9fda6be7337c86a013eeb713fe47fd29"
   },
-  {
-    "name": "first_quarter_moon_with_face",
-    "unicode": "1F31B",
+  "first_quarter_moon_with_face": {
+    "category": "nature",
+    "moji": "🌛",
+    "unicodeVersion": "6.0",
     "digest": "1d1f54a5075f2311bcc017c44898b9d8c58edc13b298d58c238fff9ab8ee2ef3"
   },
-  {
-    "name": "fish",
-    "unicode": "1F41F",
+  "fish": {
+    "category": "nature",
+    "moji": "🐟",
+    "unicodeVersion": "6.0",
     "digest": "8f62f08fbeaf39694c19816b5c7d4f292017fe5bf9f8dd7e40f1630f5f83b28b"
   },
-  {
-    "name": "fish_cake",
-    "unicode": "1F365",
+  "fish_cake": {
+    "category": "food",
+    "moji": "🍥",
+    "unicodeVersion": "6.0",
     "digest": "5a6ca2100c8830927b22afa6f1d2fc821f5692cd23507fe5a776f6e085cbbfb2"
   },
-  {
-    "name": "fishing_pole_and_fish",
-    "unicode": "1F3A3",
+  "fishing_pole_and_fish": {
+    "category": "activity",
+    "moji": "🎣",
+    "unicodeVersion": "6.0",
     "digest": "f8fb84eccceec88321b0a2a46f732ecfc378f787c19c27ac1327735f1ca9a48b"
   },
-  {
-    "name": "fist",
-    "unicode": "270A",
+  "fist": {
+    "category": "people",
+    "moji": "✊",
+    "unicodeVersion": "6.0",
     "digest": "557f96d85615b8d78436bc67266115bfc8556c97c14f7909dfda1cf134e8344f"
   },
-  {
-    "name": "fist_tone1",
-    "unicode": "270A-1F3FB",
+  "fist_tone1": {
+    "category": "people",
+    "moji": "✊🏻",
+    "unicodeVersion": "8.0",
     "digest": "6c1b946f9e01abc39b5085e24e8b6077fc0e34188e8daa30c6a3adddd387413e"
   },
-  {
-    "name": "fist_tone2",
-    "unicode": "270A-1F3FC",
+  "fist_tone2": {
+    "category": "people",
+    "moji": "✊🏼",
+    "unicodeVersion": "8.0",
     "digest": "e9b9e1ec638dca4d5e1519bca7338f58cce2f2a282ee4c3581e8643166fc415f"
   },
-  {
-    "name": "fist_tone3",
-    "unicode": "270A-1F3FD",
+  "fist_tone3": {
+    "category": "people",
+    "moji": "✊🏽",
+    "unicodeVersion": "8.0",
     "digest": "8c14d24055c143960b3d2a27fe23c55d2d3ac5f84f87e4e876616235e8698c7f"
   },
-  {
-    "name": "fist_tone4",
-    "unicode": "270A-1F3FE",
+  "fist_tone4": {
+    "category": "people",
+    "moji": "✊🏾",
+    "unicodeVersion": "8.0",
     "digest": "923f034f481e952e6e5d1664588f99f79bd5416d4197b0ade6621f2669ce5765"
   },
-  {
-    "name": "fist_tone5",
-    "unicode": "270A-1F3FF",
+  "fist_tone5": {
+    "category": "people",
+    "moji": "✊🏿",
+    "unicodeVersion": "8.0",
     "digest": "d691d2902216080916a29047e07d7a5bf2aed07e062067ca9d01cbf6fdf48c8d"
   },
-  {
-    "name": "five",
-    "unicode": "0035-20E3",
+  "five": {
+    "category": "symbols",
+    "moji": "5️⃣",
+    "unicodeVersion": "3.0",
     "digest": "8f03f62fdbf744ae49c8a60fbf715ebfccbd6b62d91148e0923907006f3c2726"
   },
-  {
-    "name": "flag_ac",
-    "unicode": "1F1E6-1F1E8",
-    "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
-  },
-  {
-    "name": "ac",
-    "unicode": "1F1E6-1F1E8",
+  "flag_ac": {
+    "category": "flags",
+    "moji": "🇦🇨",
+    "unicodeVersion": "6.0",
     "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
   },
-  {
-    "name": "flag_ad",
-    "unicode": "1F1E6-1F1E9",
-    "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
-  },
-  {
-    "name": "ad",
-    "unicode": "1F1E6-1F1E9",
+  "flag_ad": {
+    "category": "flags",
+    "moji": "🇦🇩",
+    "unicodeVersion": "6.0",
     "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
   },
-  {
-    "name": "flag_ae",
-    "unicode": "1F1E6-1F1EA",
-    "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
-  },
-  {
-    "name": "ae",
-    "unicode": "1F1E6-1F1EA",
+  "flag_ae": {
+    "category": "flags",
+    "moji": "🇦🇪",
+    "unicodeVersion": "6.0",
     "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
   },
-  {
-    "name": "flag_af",
-    "unicode": "1F1E6-1F1EB",
-    "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
-  },
-  {
-    "name": "af",
-    "unicode": "1F1E6-1F1EB",
+  "flag_af": {
+    "category": "flags",
+    "moji": "🇦🇫",
+    "unicodeVersion": "6.0",
     "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
   },
-  {
-    "name": "flag_ag",
-    "unicode": "1F1E6-1F1EC",
-    "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
-  },
-  {
-    "name": "ag",
-    "unicode": "1F1E6-1F1EC",
+  "flag_ag": {
+    "category": "flags",
+    "moji": "🇦🇬",
+    "unicodeVersion": "6.0",
     "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
   },
-  {
-    "name": "flag_ai",
-    "unicode": "1F1E6-1F1EE",
-    "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
-  },
-  {
-    "name": "ai",
-    "unicode": "1F1E6-1F1EE",
+  "flag_ai": {
+    "category": "flags",
+    "moji": "🇦🇮",
+    "unicodeVersion": "6.0",
     "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
   },
-  {
-    "name": "flag_al",
-    "unicode": "1F1E6-1F1F1",
-    "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
-  },
-  {
-    "name": "al",
-    "unicode": "1F1E6-1F1F1",
+  "flag_al": {
+    "category": "flags",
+    "moji": "🇦🇱",
+    "unicodeVersion": "6.0",
     "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
   },
-  {
-    "name": "flag_am",
-    "unicode": "1F1E6-1F1F2",
-    "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
-  },
-  {
-    "name": "am",
-    "unicode": "1F1E6-1F1F2",
+  "flag_am": {
+    "category": "flags",
+    "moji": "🇦🇲",
+    "unicodeVersion": "6.0",
     "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
   },
-  {
-    "name": "flag_ao",
-    "unicode": "1F1E6-1F1F4",
-    "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
-  },
-  {
-    "name": "ao",
-    "unicode": "1F1E6-1F1F4",
+  "flag_ao": {
+    "category": "flags",
+    "moji": "🇦🇴",
+    "unicodeVersion": "6.0",
     "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
   },
-  {
-    "name": "flag_aq",
-    "unicode": "1F1E6-1F1F6",
-    "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
-  },
-  {
-    "name": "aq",
-    "unicode": "1F1E6-1F1F6",
+  "flag_aq": {
+    "category": "flags",
+    "moji": "🇦🇶",
+    "unicodeVersion": "6.0",
     "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
   },
-  {
-    "name": "flag_ar",
-    "unicode": "1F1E6-1F1F7",
-    "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
-  },
-  {
-    "name": "ar",
-    "unicode": "1F1E6-1F1F7",
+  "flag_ar": {
+    "category": "flags",
+    "moji": "🇦🇷",
+    "unicodeVersion": "6.0",
     "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
   },
-  {
-    "name": "flag_as",
-    "unicode": "1F1E6-1F1F8",
-    "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
-  },
-  {
-    "name": "as",
-    "unicode": "1F1E6-1F1F8",
+  "flag_as": {
+    "category": "flags",
+    "moji": "🇦🇸",
+    "unicodeVersion": "6.0",
     "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
   },
-  {
-    "name": "flag_at",
-    "unicode": "1F1E6-1F1F9",
-    "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
-  },
-  {
-    "name": "at",
-    "unicode": "1F1E6-1F1F9",
+  "flag_at": {
+    "category": "flags",
+    "moji": "🇦🇹",
+    "unicodeVersion": "6.0",
     "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
   },
-  {
-    "name": "flag_au",
-    "unicode": "1F1E6-1F1FA",
-    "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
-  },
-  {
-    "name": "au",
-    "unicode": "1F1E6-1F1FA",
+  "flag_au": {
+    "category": "flags",
+    "moji": "🇦🇺",
+    "unicodeVersion": "6.0",
     "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
   },
-  {
-    "name": "flag_aw",
-    "unicode": "1F1E6-1F1FC",
-    "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
-  },
-  {
-    "name": "aw",
-    "unicode": "1F1E6-1F1FC",
+  "flag_aw": {
+    "category": "flags",
+    "moji": "🇦🇼",
+    "unicodeVersion": "6.0",
     "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
   },
-  {
-    "name": "flag_ax",
-    "unicode": "1F1E6-1F1FD",
-    "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
-  },
-  {
-    "name": "ax",
-    "unicode": "1F1E6-1F1FD",
+  "flag_ax": {
+    "category": "flags",
+    "moji": "🇦🇽",
+    "unicodeVersion": "6.0",
     "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
   },
-  {
-    "name": "flag_az",
-    "unicode": "1F1E6-1F1FF",
-    "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
-  },
-  {
-    "name": "az",
-    "unicode": "1F1E6-1F1FF",
+  "flag_az": {
+    "category": "flags",
+    "moji": "🇦🇿",
+    "unicodeVersion": "6.0",
     "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
   },
-  {
-    "name": "flag_ba",
-    "unicode": "1F1E7-1F1E6",
-    "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
-  },
-  {
-    "name": "ba",
-    "unicode": "1F1E7-1F1E6",
+  "flag_ba": {
+    "category": "flags",
+    "moji": "🇧🇦",
+    "unicodeVersion": "6.0",
     "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
   },
-  {
-    "name": "flag_bb",
-    "unicode": "1F1E7-1F1E7",
-    "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
-  },
-  {
-    "name": "bb",
-    "unicode": "1F1E7-1F1E7",
+  "flag_bb": {
+    "category": "flags",
+    "moji": "🇧🇧",
+    "unicodeVersion": "6.0",
     "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
   },
-  {
-    "name": "flag_bd",
-    "unicode": "1F1E7-1F1E9",
-    "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
-  },
-  {
-    "name": "bd",
-    "unicode": "1F1E7-1F1E9",
+  "flag_bd": {
+    "category": "flags",
+    "moji": "🇧🇩",
+    "unicodeVersion": "6.0",
     "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
   },
-  {
-    "name": "flag_be",
-    "unicode": "1F1E7-1F1EA",
-    "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
-  },
-  {
-    "name": "be",
-    "unicode": "1F1E7-1F1EA",
+  "flag_be": {
+    "category": "flags",
+    "moji": "🇧🇪",
+    "unicodeVersion": "6.0",
     "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
   },
-  {
-    "name": "flag_bf",
-    "unicode": "1F1E7-1F1EB",
-    "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
-  },
-  {
-    "name": "bf",
-    "unicode": "1F1E7-1F1EB",
+  "flag_bf": {
+    "category": "flags",
+    "moji": "🇧🇫",
+    "unicodeVersion": "6.0",
     "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
   },
-  {
-    "name": "flag_bg",
-    "unicode": "1F1E7-1F1EC",
-    "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
-  },
-  {
-    "name": "bg",
-    "unicode": "1F1E7-1F1EC",
+  "flag_bg": {
+    "category": "flags",
+    "moji": "🇧🇬",
+    "unicodeVersion": "6.0",
     "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
   },
-  {
-    "name": "flag_bh",
-    "unicode": "1F1E7-1F1ED",
-    "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
-  },
-  {
-    "name": "bh",
-    "unicode": "1F1E7-1F1ED",
+  "flag_bh": {
+    "category": "flags",
+    "moji": "🇧🇭",
+    "unicodeVersion": "6.0",
     "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
   },
-  {
-    "name": "flag_bi",
-    "unicode": "1F1E7-1F1EE",
-    "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
-  },
-  {
-    "name": "bi",
-    "unicode": "1F1E7-1F1EE",
+  "flag_bi": {
+    "category": "flags",
+    "moji": "🇧🇮",
+    "unicodeVersion": "6.0",
     "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
   },
-  {
-    "name": "flag_bj",
-    "unicode": "1F1E7-1F1EF",
-    "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
-  },
-  {
-    "name": "bj",
-    "unicode": "1F1E7-1F1EF",
+  "flag_bj": {
+    "category": "flags",
+    "moji": "🇧🇯",
+    "unicodeVersion": "6.0",
     "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
   },
-  {
-    "name": "flag_bl",
-    "unicode": "1F1E7-1F1F1",
-    "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
-  },
-  {
-    "name": "bl",
-    "unicode": "1F1E7-1F1F1",
+  "flag_bl": {
+    "category": "flags",
+    "moji": "🇧🇱",
+    "unicodeVersion": "6.0",
     "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
   },
-  {
-    "name": "flag_black",
-    "unicode": "1F3F4",
-    "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
-  },
-  {
-    "name": "waving_black_flag",
-    "unicode": "1F3F4",
+  "flag_black": {
+    "category": "objects",
+    "moji": "🏴",
+    "unicodeVersion": "6.0",
     "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
   },
-  {
-    "name": "flag_bm",
-    "unicode": "1F1E7-1F1F2",
-    "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
-  },
-  {
-    "name": "bm",
-    "unicode": "1F1E7-1F1F2",
+  "flag_bm": {
+    "category": "flags",
+    "moji": "🇧🇲",
+    "unicodeVersion": "6.0",
     "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
   },
-  {
-    "name": "flag_bn",
-    "unicode": "1F1E7-1F1F3",
-    "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
-  },
-  {
-    "name": "bn",
-    "unicode": "1F1E7-1F1F3",
+  "flag_bn": {
+    "category": "flags",
+    "moji": "🇧🇳",
+    "unicodeVersion": "6.0",
     "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
   },
-  {
-    "name": "flag_bo",
-    "unicode": "1F1E7-1F1F4",
-    "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
-  },
-  {
-    "name": "bo",
-    "unicode": "1F1E7-1F1F4",
+  "flag_bo": {
+    "category": "flags",
+    "moji": "🇧🇴",
+    "unicodeVersion": "6.0",
     "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
   },
-  {
-    "name": "flag_bq",
-    "unicode": "1F1E7-1F1F6",
-    "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
-  },
-  {
-    "name": "bq",
-    "unicode": "1F1E7-1F1F6",
+  "flag_bq": {
+    "category": "flags",
+    "moji": "🇧🇶",
+    "unicodeVersion": "6.0",
     "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
   },
-  {
-    "name": "flag_br",
-    "unicode": "1F1E7-1F1F7",
-    "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
-  },
-  {
-    "name": "br",
-    "unicode": "1F1E7-1F1F7",
+  "flag_br": {
+    "category": "flags",
+    "moji": "🇧🇷",
+    "unicodeVersion": "6.0",
     "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
   },
-  {
-    "name": "flag_bs",
-    "unicode": "1F1E7-1F1F8",
-    "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
-  },
-  {
-    "name": "bs",
-    "unicode": "1F1E7-1F1F8",
+  "flag_bs": {
+    "category": "flags",
+    "moji": "🇧🇸",
+    "unicodeVersion": "6.0",
     "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
   },
-  {
-    "name": "flag_bt",
-    "unicode": "1F1E7-1F1F9",
-    "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
-  },
-  {
-    "name": "bt",
-    "unicode": "1F1E7-1F1F9",
+  "flag_bt": {
+    "category": "flags",
+    "moji": "🇧🇹",
+    "unicodeVersion": "6.0",
     "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
   },
-  {
-    "name": "flag_bv",
-    "unicode": "1F1E7-1F1FB",
-    "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
-  },
-  {
-    "name": "bv",
-    "unicode": "1F1E7-1F1FB",
+  "flag_bv": {
+    "category": "flags",
+    "moji": "🇧🇻",
+    "unicodeVersion": "6.0",
     "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
   },
-  {
-    "name": "flag_bw",
-    "unicode": "1F1E7-1F1FC",
-    "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
-  },
-  {
-    "name": "bw",
-    "unicode": "1F1E7-1F1FC",
+  "flag_bw": {
+    "category": "flags",
+    "moji": "🇧🇼",
+    "unicodeVersion": "6.0",
     "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
   },
-  {
-    "name": "flag_by",
-    "unicode": "1F1E7-1F1FE",
-    "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
-  },
-  {
-    "name": "by",
-    "unicode": "1F1E7-1F1FE",
+  "flag_by": {
+    "category": "flags",
+    "moji": "🇧🇾",
+    "unicodeVersion": "6.0",
     "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
   },
-  {
-    "name": "flag_bz",
-    "unicode": "1F1E7-1F1FF",
-    "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
-  },
-  {
-    "name": "bz",
-    "unicode": "1F1E7-1F1FF",
+  "flag_bz": {
+    "category": "flags",
+    "moji": "🇧🇿",
+    "unicodeVersion": "6.0",
     "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
   },
-  {
-    "name": "flag_ca",
-    "unicode": "1F1E8-1F1E6",
-    "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
-  },
-  {
-    "name": "ca",
-    "unicode": "1F1E8-1F1E6",
+  "flag_ca": {
+    "category": "flags",
+    "moji": "🇨🇦",
+    "unicodeVersion": "6.0",
     "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
   },
-  {
-    "name": "flag_cc",
-    "unicode": "1F1E8-1F1E8",
-    "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
-  },
-  {
-    "name": "cc",
-    "unicode": "1F1E8-1F1E8",
+  "flag_cc": {
+    "category": "flags",
+    "moji": "🇨🇨",
+    "unicodeVersion": "6.0",
     "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
   },
-  {
-    "name": "flag_cd",
-    "unicode": "1F1E8-1F1E9",
-    "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
-  },
-  {
-    "name": "congo",
-    "unicode": "1F1E8-1F1E9",
+  "flag_cd": {
+    "category": "flags",
+    "moji": "🇨🇩",
+    "unicodeVersion": "6.0",
     "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
   },
-  {
-    "name": "flag_cf",
-    "unicode": "1F1E8-1F1EB",
-    "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
-  },
-  {
-    "name": "cf",
-    "unicode": "1F1E8-1F1EB",
+  "flag_cf": {
+    "category": "flags",
+    "moji": "🇨🇫",
+    "unicodeVersion": "6.0",
     "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
   },
-  {
-    "name": "flag_cg",
-    "unicode": "1F1E8-1F1EC",
-    "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
-  },
-  {
-    "name": "cg",
-    "unicode": "1F1E8-1F1EC",
+  "flag_cg": {
+    "category": "flags",
+    "moji": "🇨🇬",
+    "unicodeVersion": "6.0",
     "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
   },
-  {
-    "name": "flag_ch",
-    "unicode": "1F1E8-1F1ED",
-    "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
-  },
-  {
-    "name": "ch",
-    "unicode": "1F1E8-1F1ED",
+  "flag_ch": {
+    "category": "flags",
+    "moji": "🇨🇭",
+    "unicodeVersion": "6.0",
     "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
   },
-  {
-    "name": "flag_ci",
-    "unicode": "1F1E8-1F1EE",
-    "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
-  },
-  {
-    "name": "ci",
-    "unicode": "1F1E8-1F1EE",
+  "flag_ci": {
+    "category": "flags",
+    "moji": "🇨🇮",
+    "unicodeVersion": "6.0",
     "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
   },
-  {
-    "name": "flag_ck",
-    "unicode": "1F1E8-1F1F0",
-    "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
-  },
-  {
-    "name": "ck",
-    "unicode": "1F1E8-1F1F0",
+  "flag_ck": {
+    "category": "flags",
+    "moji": "🇨🇰",
+    "unicodeVersion": "6.0",
     "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
   },
-  {
-    "name": "flag_cl",
-    "unicode": "1F1E8-1F1F1",
-    "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
-  },
-  {
-    "name": "chile",
-    "unicode": "1F1E8-1F1F1",
+  "flag_cl": {
+    "category": "flags",
+    "moji": "🇨🇱",
+    "unicodeVersion": "6.0",
     "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
   },
-  {
-    "name": "flag_cm",
-    "unicode": "1F1E8-1F1F2",
-    "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
-  },
-  {
-    "name": "cm",
-    "unicode": "1F1E8-1F1F2",
+  "flag_cm": {
+    "category": "flags",
+    "moji": "🇨🇲",
+    "unicodeVersion": "6.0",
     "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
   },
-  {
-    "name": "flag_cn",
-    "unicode": "1F1E8-1F1F3",
-    "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
-  },
-  {
-    "name": "cn",
-    "unicode": "1F1E8-1F1F3",
+  "flag_cn": {
+    "category": "flags",
+    "moji": "🇨🇳",
+    "unicodeVersion": "6.0",
     "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
   },
-  {
-    "name": "flag_co",
-    "unicode": "1F1E8-1F1F4",
-    "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
-  },
-  {
-    "name": "co",
-    "unicode": "1F1E8-1F1F4",
+  "flag_co": {
+    "category": "flags",
+    "moji": "🇨🇴",
+    "unicodeVersion": "6.0",
     "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
   },
-  {
-    "name": "flag_cp",
-    "unicode": "1F1E8-1F1F5",
-    "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
-  },
-  {
-    "name": "cp",
-    "unicode": "1F1E8-1F1F5",
+  "flag_cp": {
+    "category": "flags",
+    "moji": "🇨🇵",
+    "unicodeVersion": "6.0",
     "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
   },
-  {
-    "name": "flag_cr",
-    "unicode": "1F1E8-1F1F7",
-    "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
-  },
-  {
-    "name": "cr",
-    "unicode": "1F1E8-1F1F7",
+  "flag_cr": {
+    "category": "flags",
+    "moji": "🇨🇷",
+    "unicodeVersion": "6.0",
     "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
   },
-  {
-    "name": "flag_cu",
-    "unicode": "1F1E8-1F1FA",
-    "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
-  },
-  {
-    "name": "cu",
-    "unicode": "1F1E8-1F1FA",
+  "flag_cu": {
+    "category": "flags",
+    "moji": "🇨🇺",
+    "unicodeVersion": "6.0",
     "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
   },
-  {
-    "name": "flag_cv",
-    "unicode": "1F1E8-1F1FB",
-    "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
-  },
-  {
-    "name": "cv",
-    "unicode": "1F1E8-1F1FB",
+  "flag_cv": {
+    "category": "flags",
+    "moji": "🇨🇻",
+    "unicodeVersion": "6.0",
     "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
   },
-  {
-    "name": "flag_cw",
-    "unicode": "1F1E8-1F1FC",
-    "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
-  },
-  {
-    "name": "cw",
-    "unicode": "1F1E8-1F1FC",
+  "flag_cw": {
+    "category": "flags",
+    "moji": "🇨🇼",
+    "unicodeVersion": "6.0",
     "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
   },
-  {
-    "name": "flag_cx",
-    "unicode": "1F1E8-1F1FD",
-    "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
-  },
-  {
-    "name": "cx",
-    "unicode": "1F1E8-1F1FD",
+  "flag_cx": {
+    "category": "flags",
+    "moji": "🇨🇽",
+    "unicodeVersion": "6.0",
     "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
   },
-  {
-    "name": "flag_cy",
-    "unicode": "1F1E8-1F1FE",
-    "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
-  },
-  {
-    "name": "cy",
-    "unicode": "1F1E8-1F1FE",
+  "flag_cy": {
+    "category": "flags",
+    "moji": "🇨🇾",
+    "unicodeVersion": "6.0",
     "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
   },
-  {
-    "name": "flag_cz",
-    "unicode": "1F1E8-1F1FF",
-    "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
-  },
-  {
-    "name": "cz",
-    "unicode": "1F1E8-1F1FF",
+  "flag_cz": {
+    "category": "flags",
+    "moji": "🇨🇿",
+    "unicodeVersion": "6.0",
     "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
   },
-  {
-    "name": "flag_de",
-    "unicode": "1F1E9-1F1EA",
+  "flag_de": {
+    "category": "flags",
+    "moji": "🇩🇪",
+    "unicodeVersion": "6.0",
     "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
   },
-  {
-    "name": "de",
-    "unicode": "1F1E9-1F1EA",
-    "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
-  },
-  {
-    "name": "flag_dg",
-    "unicode": "1F1E9-1F1EC",
-    "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
-  },
-  {
-    "name": "dg",
-    "unicode": "1F1E9-1F1EC",
+  "flag_dg": {
+    "category": "flags",
+    "moji": "🇩🇬",
+    "unicodeVersion": "6.0",
     "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
   },
-  {
-    "name": "flag_dj",
-    "unicode": "1F1E9-1F1EF",
-    "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
-  },
-  {
-    "name": "dj",
-    "unicode": "1F1E9-1F1EF",
+  "flag_dj": {
+    "category": "flags",
+    "moji": "🇩🇯",
+    "unicodeVersion": "6.0",
     "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
   },
-  {
-    "name": "flag_dk",
-    "unicode": "1F1E9-1F1F0",
+  "flag_dk": {
+    "category": "flags",
+    "moji": "🇩🇰",
+    "unicodeVersion": "6.0",
     "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
   },
-  {
-    "name": "dk",
-    "unicode": "1F1E9-1F1F0",
-    "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
-  },
-  {
-    "name": "flag_dm",
-    "unicode": "1F1E9-1F1F2",
+  "flag_dm": {
+    "category": "flags",
+    "moji": "🇩🇲",
+    "unicodeVersion": "6.0",
     "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
   },
-  {
-    "name": "dm",
-    "unicode": "1F1E9-1F1F2",
-    "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
-  },
-  {
-    "name": "flag_do",
-    "unicode": "1F1E9-1F1F4",
+  "flag_do": {
+    "category": "flags",
+    "moji": "🇩🇴",
+    "unicodeVersion": "6.0",
     "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
   },
-  {
-    "name": "do",
-    "unicode": "1F1E9-1F1F4",
-    "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
-  },
-  {
-    "name": "flag_dz",
-    "unicode": "1F1E9-1F1FF",
-    "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
-  },
-  {
-    "name": "dz",
-    "unicode": "1F1E9-1F1FF",
+  "flag_dz": {
+    "category": "flags",
+    "moji": "🇩🇿",
+    "unicodeVersion": "6.0",
     "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
   },
-  {
-    "name": "flag_ea",
-    "unicode": "1F1EA-1F1E6",
-    "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
-  },
-  {
-    "name": "ea",
-    "unicode": "1F1EA-1F1E6",
+  "flag_ea": {
+    "category": "flags",
+    "moji": "🇪🇦",
+    "unicodeVersion": "6.0",
     "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
   },
-  {
-    "name": "flag_ec",
-    "unicode": "1F1EA-1F1E8",
+  "flag_ec": {
+    "category": "flags",
+    "moji": "🇪🇨",
+    "unicodeVersion": "6.0",
     "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
   },
-  {
-    "name": "ec",
-    "unicode": "1F1EA-1F1E8",
-    "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
-  },
-  {
-    "name": "flag_ee",
-    "unicode": "1F1EA-1F1EA",
-    "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
-  },
-  {
-    "name": "ee",
-    "unicode": "1F1EA-1F1EA",
+  "flag_ee": {
+    "category": "flags",
+    "moji": "🇪🇪",
+    "unicodeVersion": "6.0",
     "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
   },
-  {
-    "name": "flag_eg",
-    "unicode": "1F1EA-1F1EC",
+  "flag_eg": {
+    "category": "flags",
+    "moji": "🇪🇬",
+    "unicodeVersion": "6.0",
     "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
   },
-  {
-    "name": "eg",
-    "unicode": "1F1EA-1F1EC",
-    "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
-  },
-  {
-    "name": "flag_eh",
-    "unicode": "1F1EA-1F1ED",
+  "flag_eh": {
+    "category": "flags",
+    "moji": "🇪🇭",
+    "unicodeVersion": "6.0",
     "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
   },
-  {
-    "name": "eh",
-    "unicode": "1F1EA-1F1ED",
-    "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
-  },
-  {
-    "name": "flag_er",
-    "unicode": "1F1EA-1F1F7",
-    "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
-  },
-  {
-    "name": "er",
-    "unicode": "1F1EA-1F1F7",
+  "flag_er": {
+    "category": "flags",
+    "moji": "🇪🇷",
+    "unicodeVersion": "6.0",
     "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
   },
-  {
-    "name": "flag_es",
-    "unicode": "1F1EA-1F1F8",
-    "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
-  },
-  {
-    "name": "es",
-    "unicode": "1F1EA-1F1F8",
+  "flag_es": {
+    "category": "flags",
+    "moji": "🇪🇸",
+    "unicodeVersion": "6.0",
     "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
   },
-  {
-    "name": "flag_et",
-    "unicode": "1F1EA-1F1F9",
-    "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
-  },
-  {
-    "name": "et",
-    "unicode": "1F1EA-1F1F9",
+  "flag_et": {
+    "category": "flags",
+    "moji": "🇪🇹",
+    "unicodeVersion": "6.0",
     "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
   },
-  {
-    "name": "flag_eu",
-    "unicode": "1F1EA-1F1FA",
+  "flag_eu": {
+    "category": "flags",
+    "moji": "🇪🇺",
+    "unicodeVersion": "6.0",
     "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
   },
-  {
-    "name": "eu",
-    "unicode": "1F1EA-1F1FA",
-    "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
-  },
-  {
-    "name": "flag_fi",
-    "unicode": "1F1EB-1F1EE",
+  "flag_fi": {
+    "category": "flags",
+    "moji": "🇫🇮",
+    "unicodeVersion": "6.0",
     "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
   },
-  {
-    "name": "fi",
-    "unicode": "1F1EB-1F1EE",
-    "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
-  },
-  {
-    "name": "flag_fj",
-    "unicode": "1F1EB-1F1EF",
+  "flag_fj": {
+    "category": "flags",
+    "moji": "🇫🇯",
+    "unicodeVersion": "6.0",
     "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
   },
-  {
-    "name": "fj",
-    "unicode": "1F1EB-1F1EF",
-    "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
-  },
-  {
-    "name": "flag_fk",
-    "unicode": "1F1EB-1F1F0",
-    "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
-  },
-  {
-    "name": "fk",
-    "unicode": "1F1EB-1F1F0",
+  "flag_fk": {
+    "category": "flags",
+    "moji": "🇫🇰",
+    "unicodeVersion": "6.0",
     "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
   },
-  {
-    "name": "flag_fm",
-    "unicode": "1F1EB-1F1F2",
+  "flag_fm": {
+    "category": "flags",
+    "moji": "🇫🇲",
+    "unicodeVersion": "6.0",
     "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
   },
-  {
-    "name": "fm",
-    "unicode": "1F1EB-1F1F2",
-    "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
-  },
-  {
-    "name": "flag_fo",
-    "unicode": "1F1EB-1F1F4",
-    "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
-  },
-  {
-    "name": "fo",
-    "unicode": "1F1EB-1F1F4",
+  "flag_fo": {
+    "category": "flags",
+    "moji": "🇫🇴",
+    "unicodeVersion": "6.0",
     "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
   },
-  {
-    "name": "flag_fr",
-    "unicode": "1F1EB-1F1F7",
-    "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
-  },
-  {
-    "name": "fr",
-    "unicode": "1F1EB-1F1F7",
+  "flag_fr": {
+    "category": "flags",
+    "moji": "🇫🇷",
+    "unicodeVersion": "6.0",
     "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
   },
-  {
-    "name": "flag_ga",
-    "unicode": "1F1EC-1F1E6",
+  "flag_ga": {
+    "category": "flags",
+    "moji": "🇬🇦",
+    "unicodeVersion": "6.0",
     "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
   },
-  {
-    "name": "ga",
-    "unicode": "1F1EC-1F1E6",
-    "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
-  },
-  {
-    "name": "flag_gb",
-    "unicode": "1F1EC-1F1E7",
+  "flag_gb": {
+    "category": "flags",
+    "moji": "🇬🇧",
+    "unicodeVersion": "6.0",
     "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
   },
-  {
-    "name": "gb",
-    "unicode": "1F1EC-1F1E7",
-    "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
-  },
-  {
-    "name": "flag_gd",
-    "unicode": "1F1EC-1F1E9",
+  "flag_gd": {
+    "category": "flags",
+    "moji": "🇬🇩",
+    "unicodeVersion": "6.0",
     "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
   },
-  {
-    "name": "gd",
-    "unicode": "1F1EC-1F1E9",
-    "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
-  },
-  {
-    "name": "flag_ge",
-    "unicode": "1F1EC-1F1EA",
-    "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
-  },
-  {
-    "name": "ge",
-    "unicode": "1F1EC-1F1EA",
+  "flag_ge": {
+    "category": "flags",
+    "moji": "🇬🇪",
+    "unicodeVersion": "6.0",
     "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
   },
-  {
-    "name": "flag_gf",
-    "unicode": "1F1EC-1F1EB",
-    "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
-  },
-  {
-    "name": "gf",
-    "unicode": "1F1EC-1F1EB",
+  "flag_gf": {
+    "category": "flags",
+    "moji": "🇬🇫",
+    "unicodeVersion": "6.0",
     "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
   },
-  {
-    "name": "flag_gg",
-    "unicode": "1F1EC-1F1EC",
+  "flag_gg": {
+    "category": "flags",
+    "moji": "🇬🇬",
+    "unicodeVersion": "6.0",
     "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
   },
-  {
-    "name": "gg",
-    "unicode": "1F1EC-1F1EC",
-    "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
-  },
-  {
-    "name": "flag_gh",
-    "unicode": "1F1EC-1F1ED",
-    "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
-  },
-  {
-    "name": "gh",
-    "unicode": "1F1EC-1F1ED",
+  "flag_gh": {
+    "category": "flags",
+    "moji": "🇬🇭",
+    "unicodeVersion": "6.0",
     "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
   },
-  {
-    "name": "flag_gi",
-    "unicode": "1F1EC-1F1EE",
+  "flag_gi": {
+    "category": "flags",
+    "moji": "🇬🇮",
+    "unicodeVersion": "6.0",
     "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
   },
-  {
-    "name": "gi",
-    "unicode": "1F1EC-1F1EE",
-    "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
-  },
-  {
-    "name": "flag_gl",
-    "unicode": "1F1EC-1F1F1",
+  "flag_gl": {
+    "category": "flags",
+    "moji": "🇬🇱",
+    "unicodeVersion": "6.0",
     "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
   },
-  {
-    "name": "gl",
-    "unicode": "1F1EC-1F1F1",
-    "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
-  },
-  {
-    "name": "flag_gm",
-    "unicode": "1F1EC-1F1F2",
+  "flag_gm": {
+    "category": "flags",
+    "moji": "🇬🇲",
+    "unicodeVersion": "6.0",
     "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
   },
-  {
-    "name": "gm",
-    "unicode": "1F1EC-1F1F2",
-    "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
-  },
-  {
-    "name": "flag_gn",
-    "unicode": "1F1EC-1F1F3",
-    "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
-  },
-  {
-    "name": "gn",
-    "unicode": "1F1EC-1F1F3",
+  "flag_gn": {
+    "category": "flags",
+    "moji": "🇬🇳",
+    "unicodeVersion": "6.0",
     "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
   },
-  {
-    "name": "flag_gp",
-    "unicode": "1F1EC-1F1F5",
-    "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
-  },
-  {
-    "name": "gp",
-    "unicode": "1F1EC-1F1F5",
+  "flag_gp": {
+    "category": "flags",
+    "moji": "🇬🇵",
+    "unicodeVersion": "6.0",
     "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
   },
-  {
-    "name": "flag_gq",
-    "unicode": "1F1EC-1F1F6",
-    "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
-  },
-  {
-    "name": "gq",
-    "unicode": "1F1EC-1F1F6",
+  "flag_gq": {
+    "category": "flags",
+    "moji": "🇬🇶",
+    "unicodeVersion": "6.0",
     "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
   },
-  {
-    "name": "flag_gr",
-    "unicode": "1F1EC-1F1F7",
-    "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
-  },
-  {
-    "name": "gr",
-    "unicode": "1F1EC-1F1F7",
+  "flag_gr": {
+    "category": "flags",
+    "moji": "🇬🇷",
+    "unicodeVersion": "6.0",
     "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
   },
-  {
-    "name": "flag_gs",
-    "unicode": "1F1EC-1F1F8",
+  "flag_gs": {
+    "category": "flags",
+    "moji": "🇬🇸",
+    "unicodeVersion": "6.0",
     "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
   },
-  {
-    "name": "gs",
-    "unicode": "1F1EC-1F1F8",
-    "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
-  },
-  {
-    "name": "flag_gt",
-    "unicode": "1F1EC-1F1F9",
-    "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
-  },
-  {
-    "name": "gt",
-    "unicode": "1F1EC-1F1F9",
+  "flag_gt": {
+    "category": "flags",
+    "moji": "🇬🇹",
+    "unicodeVersion": "6.0",
     "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
   },
-  {
-    "name": "flag_gu",
-    "unicode": "1F1EC-1F1FA",
+  "flag_gu": {
+    "category": "flags",
+    "moji": "🇬🇺",
+    "unicodeVersion": "6.0",
     "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
   },
-  {
-    "name": "gu",
-    "unicode": "1F1EC-1F1FA",
-    "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
-  },
-  {
-    "name": "flag_gw",
-    "unicode": "1F1EC-1F1FC",
+  "flag_gw": {
+    "category": "flags",
+    "moji": "🇬🇼",
+    "unicodeVersion": "6.0",
     "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
   },
-  {
-    "name": "gw",
-    "unicode": "1F1EC-1F1FC",
-    "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
-  },
-  {
-    "name": "flag_gy",
-    "unicode": "1F1EC-1F1FE",
-    "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
-  },
-  {
-    "name": "gy",
-    "unicode": "1F1EC-1F1FE",
+  "flag_gy": {
+    "category": "flags",
+    "moji": "🇬🇾",
+    "unicodeVersion": "6.0",
     "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
   },
-  {
-    "name": "flag_hk",
-    "unicode": "1F1ED-1F1F0",
-    "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
-  },
-  {
-    "name": "hk",
-    "unicode": "1F1ED-1F1F0",
+  "flag_hk": {
+    "category": "flags",
+    "moji": "🇭🇰",
+    "unicodeVersion": "6.0",
     "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
   },
-  {
-    "name": "flag_hm",
-    "unicode": "1F1ED-1F1F2",
-    "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
-  },
-  {
-    "name": "hm",
-    "unicode": "1F1ED-1F1F2",
+  "flag_hm": {
+    "category": "flags",
+    "moji": "🇭🇲",
+    "unicodeVersion": "6.0",
     "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
   },
-  {
-    "name": "flag_hn",
-    "unicode": "1F1ED-1F1F3",
+  "flag_hn": {
+    "category": "flags",
+    "moji": "🇭🇳",
+    "unicodeVersion": "6.0",
     "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
   },
-  {
-    "name": "hn",
-    "unicode": "1F1ED-1F1F3",
-    "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
-  },
-  {
-    "name": "flag_hr",
-    "unicode": "1F1ED-1F1F7",
+  "flag_hr": {
+    "category": "flags",
+    "moji": "🇭🇷",
+    "unicodeVersion": "6.0",
     "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
   },
-  {
-    "name": "hr",
-    "unicode": "1F1ED-1F1F7",
-    "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
-  },
-  {
-    "name": "flag_ht",
-    "unicode": "1F1ED-1F1F9",
+  "flag_ht": {
+    "category": "flags",
+    "moji": "🇭🇹",
+    "unicodeVersion": "6.0",
     "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
   },
-  {
-    "name": "ht",
-    "unicode": "1F1ED-1F1F9",
-    "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
-  },
-  {
-    "name": "flag_hu",
-    "unicode": "1F1ED-1F1FA",
-    "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
-  },
-  {
-    "name": "hu",
-    "unicode": "1F1ED-1F1FA",
+  "flag_hu": {
+    "category": "flags",
+    "moji": "🇭🇺",
+    "unicodeVersion": "6.0",
     "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
   },
-  {
-    "name": "flag_ic",
-    "unicode": "1F1EE-1F1E8",
+  "flag_ic": {
+    "category": "flags",
+    "moji": "🇮🇨",
+    "unicodeVersion": "6.0",
     "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
   },
-  {
-    "name": "ic",
-    "unicode": "1F1EE-1F1E8",
-    "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
-  },
-  {
-    "name": "flag_id",
-    "unicode": "1F1EE-1F1E9",
-    "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
-  },
-  {
-    "name": "indonesia",
-    "unicode": "1F1EE-1F1E9",
+  "flag_id": {
+    "category": "flags",
+    "moji": "🇮🇩",
+    "unicodeVersion": "6.0",
     "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
   },
-  {
-    "name": "flag_ie",
-    "unicode": "1F1EE-1F1EA",
-    "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
-  },
-  {
-    "name": "ie",
-    "unicode": "1F1EE-1F1EA",
+  "flag_ie": {
+    "category": "flags",
+    "moji": "🇮🇪",
+    "unicodeVersion": "6.0",
     "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
   },
-  {
-    "name": "flag_il",
-    "unicode": "1F1EE-1F1F1",
+  "flag_il": {
+    "category": "flags",
+    "moji": "🇮🇱",
+    "unicodeVersion": "6.0",
     "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
   },
-  {
-    "name": "il",
-    "unicode": "1F1EE-1F1F1",
-    "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
-  },
-  {
-    "name": "flag_im",
-    "unicode": "1F1EE-1F1F2",
-    "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
-  },
-  {
-    "name": "im",
-    "unicode": "1F1EE-1F1F2",
+  "flag_im": {
+    "category": "flags",
+    "moji": "🇮🇲",
+    "unicodeVersion": "6.0",
     "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
   },
-  {
-    "name": "flag_in",
-    "unicode": "1F1EE-1F1F3",
+  "flag_in": {
+    "category": "flags",
+    "moji": "🇮🇳",
+    "unicodeVersion": "6.0",
     "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
   },
-  {
-    "name": "in",
-    "unicode": "1F1EE-1F1F3",
-    "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
-  },
-  {
-    "name": "flag_io",
-    "unicode": "1F1EE-1F1F4",
+  "flag_io": {
+    "category": "flags",
+    "moji": "🇮🇴",
+    "unicodeVersion": "6.0",
     "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
   },
-  {
-    "name": "io",
-    "unicode": "1F1EE-1F1F4",
-    "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
-  },
-  {
-    "name": "flag_iq",
-    "unicode": "1F1EE-1F1F6",
+  "flag_iq": {
+    "category": "flags",
+    "moji": "🇮🇶",
+    "unicodeVersion": "6.0",
     "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
   },
-  {
-    "name": "iq",
-    "unicode": "1F1EE-1F1F6",
-    "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
-  },
-  {
-    "name": "flag_ir",
-    "unicode": "1F1EE-1F1F7",
-    "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
-  },
-  {
-    "name": "ir",
-    "unicode": "1F1EE-1F1F7",
+  "flag_ir": {
+    "category": "flags",
+    "moji": "🇮🇷",
+    "unicodeVersion": "6.0",
     "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
   },
-  {
-    "name": "flag_is",
-    "unicode": "1F1EE-1F1F8",
-    "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
-  },
-  {
-    "name": "is",
-    "unicode": "1F1EE-1F1F8",
+  "flag_is": {
+    "category": "flags",
+    "moji": "🇮🇸",
+    "unicodeVersion": "6.0",
     "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
   },
-  {
-    "name": "flag_it",
-    "unicode": "1F1EE-1F1F9",
-    "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
-  },
-  {
-    "name": "it",
-    "unicode": "1F1EE-1F1F9",
+  "flag_it": {
+    "category": "flags",
+    "moji": "🇮🇹",
+    "unicodeVersion": "6.0",
     "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
   },
-  {
-    "name": "flag_je",
-    "unicode": "1F1EF-1F1EA",
-    "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
-  },
-  {
-    "name": "je",
-    "unicode": "1F1EF-1F1EA",
+  "flag_je": {
+    "category": "flags",
+    "moji": "🇯🇪",
+    "unicodeVersion": "6.0",
     "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
   },
-  {
-    "name": "flag_jm",
-    "unicode": "1F1EF-1F1F2",
+  "flag_jm": {
+    "category": "flags",
+    "moji": "🇯🇲",
+    "unicodeVersion": "6.0",
     "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
   },
-  {
-    "name": "jm",
-    "unicode": "1F1EF-1F1F2",
-    "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
-  },
-  {
-    "name": "flag_jo",
-    "unicode": "1F1EF-1F1F4",
+  "flag_jo": {
+    "category": "flags",
+    "moji": "🇯🇴",
+    "unicodeVersion": "6.0",
     "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
   },
-  {
-    "name": "jo",
-    "unicode": "1F1EF-1F1F4",
-    "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
-  },
-  {
-    "name": "flag_jp",
-    "unicode": "1F1EF-1F1F5",
+  "flag_jp": {
+    "category": "flags",
+    "moji": "🇯🇵",
+    "unicodeVersion": "6.0",
     "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
   },
-  {
-    "name": "jp",
-    "unicode": "1F1EF-1F1F5",
-    "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
-  },
-  {
-    "name": "flag_ke",
-    "unicode": "1F1F0-1F1EA",
-    "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
-  },
-  {
-    "name": "ke",
-    "unicode": "1F1F0-1F1EA",
+  "flag_ke": {
+    "category": "flags",
+    "moji": "🇰🇪",
+    "unicodeVersion": "6.0",
     "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
   },
-  {
-    "name": "flag_kg",
-    "unicode": "1F1F0-1F1EC",
-    "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
-  },
-  {
-    "name": "kg",
-    "unicode": "1F1F0-1F1EC",
+  "flag_kg": {
+    "category": "flags",
+    "moji": "🇰🇬",
+    "unicodeVersion": "6.0",
     "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
   },
-  {
-    "name": "flag_kh",
-    "unicode": "1F1F0-1F1ED",
+  "flag_kh": {
+    "category": "flags",
+    "moji": "🇰🇭",
+    "unicodeVersion": "6.0",
     "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
   },
-  {
-    "name": "kh",
-    "unicode": "1F1F0-1F1ED",
-    "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
-  },
-  {
-    "name": "flag_ki",
-    "unicode": "1F1F0-1F1EE",
-    "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
-  },
-  {
-    "name": "ki",
-    "unicode": "1F1F0-1F1EE",
+  "flag_ki": {
+    "category": "flags",
+    "moji": "🇰🇮",
+    "unicodeVersion": "6.0",
     "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
   },
-  {
-    "name": "flag_km",
-    "unicode": "1F1F0-1F1F2",
+  "flag_km": {
+    "category": "flags",
+    "moji": "🇰🇲",
+    "unicodeVersion": "6.0",
     "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
   },
-  {
-    "name": "km",
-    "unicode": "1F1F0-1F1F2",
-    "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
-  },
-  {
-    "name": "flag_kn",
-    "unicode": "1F1F0-1F1F3",
+  "flag_kn": {
+    "category": "flags",
+    "moji": "🇰🇳",
+    "unicodeVersion": "6.0",
     "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
   },
-  {
-    "name": "kn",
-    "unicode": "1F1F0-1F1F3",
-    "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
-  },
-  {
-    "name": "flag_kp",
-    "unicode": "1F1F0-1F1F5",
+  "flag_kp": {
+    "category": "flags",
+    "moji": "🇰🇵",
+    "unicodeVersion": "6.0",
     "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
   },
-  {
-    "name": "kp",
-    "unicode": "1F1F0-1F1F5",
-    "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
-  },
-  {
-    "name": "flag_kr",
-    "unicode": "1F1F0-1F1F7",
-    "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
-  },
-  {
-    "name": "kr",
-    "unicode": "1F1F0-1F1F7",
+  "flag_kr": {
+    "category": "flags",
+    "moji": "🇰🇷",
+    "unicodeVersion": "6.0",
     "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
   },
-  {
-    "name": "flag_kw",
-    "unicode": "1F1F0-1F1FC",
-    "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
-  },
-  {
-    "name": "kw",
-    "unicode": "1F1F0-1F1FC",
+  "flag_kw": {
+    "category": "flags",
+    "moji": "🇰🇼",
+    "unicodeVersion": "6.0",
     "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
   },
-  {
-    "name": "flag_ky",
-    "unicode": "1F1F0-1F1FE",
-    "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
-  },
-  {
-    "name": "ky",
-    "unicode": "1F1F0-1F1FE",
+  "flag_ky": {
+    "category": "flags",
+    "moji": "🇰🇾",
+    "unicodeVersion": "6.0",
     "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
   },
-  {
-    "name": "flag_kz",
-    "unicode": "1F1F0-1F1FF",
-    "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
-  },
-  {
-    "name": "kz",
-    "unicode": "1F1F0-1F1FF",
+  "flag_kz": {
+    "category": "flags",
+    "moji": "🇰🇿",
+    "unicodeVersion": "6.0",
     "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
   },
-  {
-    "name": "flag_la",
-    "unicode": "1F1F1-1F1E6",
+  "flag_la": {
+    "category": "flags",
+    "moji": "🇱🇦",
+    "unicodeVersion": "6.0",
     "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
   },
-  {
-    "name": "la",
-    "unicode": "1F1F1-1F1E6",
-    "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
-  },
-  {
-    "name": "flag_lb",
-    "unicode": "1F1F1-1F1E7",
-    "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
-  },
-  {
-    "name": "lb",
-    "unicode": "1F1F1-1F1E7",
+  "flag_lb": {
+    "category": "flags",
+    "moji": "🇱🇧",
+    "unicodeVersion": "6.0",
     "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
   },
-  {
-    "name": "flag_lc",
-    "unicode": "1F1F1-1F1E8",
+  "flag_lc": {
+    "category": "flags",
+    "moji": "🇱🇨",
+    "unicodeVersion": "6.0",
     "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
   },
-  {
-    "name": "lc",
-    "unicode": "1F1F1-1F1E8",
-    "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
-  },
-  {
-    "name": "flag_li",
-    "unicode": "1F1F1-1F1EE",
+  "flag_li": {
+    "category": "flags",
+    "moji": "🇱🇮",
+    "unicodeVersion": "6.0",
     "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
   },
-  {
-    "name": "li",
-    "unicode": "1F1F1-1F1EE",
-    "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
-  },
-  {
-    "name": "flag_lk",
-    "unicode": "1F1F1-1F1F0",
-    "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
-  },
-  {
-    "name": "lk",
-    "unicode": "1F1F1-1F1F0",
+  "flag_lk": {
+    "category": "flags",
+    "moji": "🇱🇰",
+    "unicodeVersion": "6.0",
     "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
   },
-  {
-    "name": "flag_lr",
-    "unicode": "1F1F1-1F1F7",
-    "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
-  },
-  {
-    "name": "lr",
-    "unicode": "1F1F1-1F1F7",
+  "flag_lr": {
+    "category": "flags",
+    "moji": "🇱🇷",
+    "unicodeVersion": "6.0",
     "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
   },
-  {
-    "name": "flag_ls",
-    "unicode": "1F1F1-1F1F8",
-    "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
-  },
-  {
-    "name": "ls",
-    "unicode": "1F1F1-1F1F8",
+  "flag_ls": {
+    "category": "flags",
+    "moji": "🇱🇸",
+    "unicodeVersion": "6.0",
     "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
   },
-  {
-    "name": "flag_lt",
-    "unicode": "1F1F1-1F1F9",
+  "flag_lt": {
+    "category": "flags",
+    "moji": "🇱🇹",
+    "unicodeVersion": "6.0",
     "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
   },
-  {
-    "name": "lt",
-    "unicode": "1F1F1-1F1F9",
-    "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
-  },
-  {
-    "name": "flag_lu",
-    "unicode": "1F1F1-1F1FA",
+  "flag_lu": {
+    "category": "flags",
+    "moji": "🇱🇺",
+    "unicodeVersion": "6.0",
     "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
   },
-  {
-    "name": "lu",
-    "unicode": "1F1F1-1F1FA",
-    "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
-  },
-  {
-    "name": "flag_lv",
-    "unicode": "1F1F1-1F1FB",
+  "flag_lv": {
+    "category": "flags",
+    "moji": "🇱🇻",
+    "unicodeVersion": "6.0",
     "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
   },
-  {
-    "name": "lv",
-    "unicode": "1F1F1-1F1FB",
-    "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
-  },
-  {
-    "name": "flag_ly",
-    "unicode": "1F1F1-1F1FE",
-    "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
-  },
-  {
-    "name": "ly",
-    "unicode": "1F1F1-1F1FE",
+  "flag_ly": {
+    "category": "flags",
+    "moji": "🇱🇾",
+    "unicodeVersion": "6.0",
     "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
   },
-  {
-    "name": "flag_ma",
-    "unicode": "1F1F2-1F1E6",
+  "flag_ma": {
+    "category": "flags",
+    "moji": "🇲🇦",
+    "unicodeVersion": "6.0",
     "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
   },
-  {
-    "name": "ma",
-    "unicode": "1F1F2-1F1E6",
-    "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
-  },
-  {
-    "name": "flag_mc",
-    "unicode": "1F1F2-1F1E8",
-    "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
-  },
-  {
-    "name": "mc",
-    "unicode": "1F1F2-1F1E8",
+  "flag_mc": {
+    "category": "flags",
+    "moji": "🇲🇨",
+    "unicodeVersion": "6.0",
     "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
   },
-  {
-    "name": "flag_md",
-    "unicode": "1F1F2-1F1E9",
-    "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
-  },
-  {
-    "name": "md",
-    "unicode": "1F1F2-1F1E9",
+  "flag_md": {
+    "category": "flags",
+    "moji": "🇲🇩",
+    "unicodeVersion": "6.0",
     "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
   },
-  {
-    "name": "flag_me",
-    "unicode": "1F1F2-1F1EA",
+  "flag_me": {
+    "category": "flags",
+    "moji": "🇲🇪",
+    "unicodeVersion": "6.0",
     "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
   },
-  {
-    "name": "me",
-    "unicode": "1F1F2-1F1EA",
-    "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
-  },
-  {
-    "name": "flag_mf",
-    "unicode": "1F1F2-1F1EB",
-    "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
-  },
-  {
-    "name": "mf",
-    "unicode": "1F1F2-1F1EB",
+  "flag_mf": {
+    "category": "flags",
+    "moji": "🇲🇫",
+    "unicodeVersion": "6.0",
     "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
   },
-  {
-    "name": "flag_mg",
-    "unicode": "1F1F2-1F1EC",
+  "flag_mg": {
+    "category": "flags",
+    "moji": "🇲🇬",
+    "unicodeVersion": "6.0",
     "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
   },
-  {
-    "name": "mg",
-    "unicode": "1F1F2-1F1EC",
-    "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
-  },
-  {
-    "name": "flag_mh",
-    "unicode": "1F1F2-1F1ED",
+  "flag_mh": {
+    "category": "flags",
+    "moji": "🇲🇭",
+    "unicodeVersion": "6.0",
     "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
   },
-  {
-    "name": "mh",
-    "unicode": "1F1F2-1F1ED",
-    "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
-  },
-  {
-    "name": "flag_mk",
-    "unicode": "1F1F2-1F1F0",
+  "flag_mk": {
+    "category": "flags",
+    "moji": "🇲🇰",
+    "unicodeVersion": "6.0",
     "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
   },
-  {
-    "name": "mk",
-    "unicode": "1F1F2-1F1F0",
-    "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
-  },
-  {
-    "name": "flag_ml",
-    "unicode": "1F1F2-1F1F1",
-    "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
-  },
-  {
-    "name": "ml",
-    "unicode": "1F1F2-1F1F1",
+  "flag_ml": {
+    "category": "flags",
+    "moji": "🇲🇱",
+    "unicodeVersion": "6.0",
     "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
   },
-  {
-    "name": "flag_mm",
-    "unicode": "1F1F2-1F1F2",
-    "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
-  },
-  {
-    "name": "mm",
-    "unicode": "1F1F2-1F1F2",
+  "flag_mm": {
+    "category": "flags",
+    "moji": "🇲🇲",
+    "unicodeVersion": "6.0",
     "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
   },
-  {
-    "name": "flag_mn",
-    "unicode": "1F1F2-1F1F3",
-    "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
-  },
-  {
-    "name": "mn",
-    "unicode": "1F1F2-1F1F3",
+  "flag_mn": {
+    "category": "flags",
+    "moji": "🇲🇳",
+    "unicodeVersion": "6.0",
     "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
   },
-  {
-    "name": "flag_mo",
-    "unicode": "1F1F2-1F1F4",
-    "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
-  },
-  {
-    "name": "mo",
-    "unicode": "1F1F2-1F1F4",
+  "flag_mo": {
+    "category": "flags",
+    "moji": "🇲🇴",
+    "unicodeVersion": "6.0",
     "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
   },
-  {
-    "name": "flag_mp",
-    "unicode": "1F1F2-1F1F5",
+  "flag_mp": {
+    "category": "flags",
+    "moji": "🇲🇵",
+    "unicodeVersion": "6.0",
     "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
   },
-  {
-    "name": "mp",
-    "unicode": "1F1F2-1F1F5",
-    "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
-  },
-  {
-    "name": "flag_mq",
-    "unicode": "1F1F2-1F1F6",
+  "flag_mq": {
+    "category": "flags",
+    "moji": "🇲🇶",
+    "unicodeVersion": "6.0",
     "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
   },
-  {
-    "name": "mq",
-    "unicode": "1F1F2-1F1F6",
-    "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
-  },
-  {
-    "name": "flag_mr",
-    "unicode": "1F1F2-1F1F7",
+  "flag_mr": {
+    "category": "flags",
+    "moji": "🇲🇷",
+    "unicodeVersion": "6.0",
     "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
   },
-  {
-    "name": "mr",
-    "unicode": "1F1F2-1F1F7",
-    "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
-  },
-  {
-    "name": "flag_ms",
-    "unicode": "1F1F2-1F1F8",
-    "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
-  },
-  {
-    "name": "ms",
-    "unicode": "1F1F2-1F1F8",
+  "flag_ms": {
+    "category": "flags",
+    "moji": "🇲🇸",
+    "unicodeVersion": "6.0",
     "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
   },
-  {
-    "name": "flag_mt",
-    "unicode": "1F1F2-1F1F9",
-    "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
-  },
-  {
-    "name": "mt",
-    "unicode": "1F1F2-1F1F9",
+  "flag_mt": {
+    "category": "flags",
+    "moji": "🇲🇹",
+    "unicodeVersion": "6.0",
     "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
   },
-  {
-    "name": "flag_mu",
-    "unicode": "1F1F2-1F1FA",
+  "flag_mu": {
+    "category": "flags",
+    "moji": "🇲🇺",
+    "unicodeVersion": "6.0",
     "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
   },
-  {
-    "name": "mu",
-    "unicode": "1F1F2-1F1FA",
-    "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
-  },
-  {
-    "name": "flag_mv",
-    "unicode": "1F1F2-1F1FB",
-    "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
-  },
-  {
-    "name": "mv",
-    "unicode": "1F1F2-1F1FB",
+  "flag_mv": {
+    "category": "flags",
+    "moji": "🇲🇻",
+    "unicodeVersion": "6.0",
     "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
   },
-  {
-    "name": "flag_mw",
-    "unicode": "1F1F2-1F1FC",
+  "flag_mw": {
+    "category": "flags",
+    "moji": "🇲🇼",
+    "unicodeVersion": "6.0",
     "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
   },
-  {
-    "name": "mw",
-    "unicode": "1F1F2-1F1FC",
-    "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
-  },
-  {
-    "name": "flag_mx",
-    "unicode": "1F1F2-1F1FD",
+  "flag_mx": {
+    "category": "flags",
+    "moji": "🇲🇽",
+    "unicodeVersion": "6.0",
     "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
   },
-  {
-    "name": "mx",
-    "unicode": "1F1F2-1F1FD",
-    "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
-  },
-  {
-    "name": "flag_my",
-    "unicode": "1F1F2-1F1FE",
+  "flag_my": {
+    "category": "flags",
+    "moji": "🇲🇾",
+    "unicodeVersion": "6.0",
     "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
   },
-  {
-    "name": "my",
-    "unicode": "1F1F2-1F1FE",
-    "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
-  },
-  {
-    "name": "flag_mz",
-    "unicode": "1F1F2-1F1FF",
-    "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
-  },
-  {
-    "name": "mz",
-    "unicode": "1F1F2-1F1FF",
+  "flag_mz": {
+    "category": "flags",
+    "moji": "🇲🇿",
+    "unicodeVersion": "6.0",
     "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
   },
-  {
-    "name": "flag_na",
-    "unicode": "1F1F3-1F1E6",
-    "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
-  },
-  {
-    "name": "na",
-    "unicode": "1F1F3-1F1E6",
+  "flag_na": {
+    "category": "flags",
+    "moji": "🇳🇦",
+    "unicodeVersion": "6.0",
     "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
   },
-  {
-    "name": "flag_nc",
-    "unicode": "1F1F3-1F1E8",
-    "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
-  },
-  {
-    "name": "nc",
-    "unicode": "1F1F3-1F1E8",
+  "flag_nc": {
+    "category": "flags",
+    "moji": "🇳🇨",
+    "unicodeVersion": "6.0",
     "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
   },
-  {
-    "name": "flag_ne",
-    "unicode": "1F1F3-1F1EA",
-    "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
-  },
-  {
-    "name": "ne",
-    "unicode": "1F1F3-1F1EA",
+  "flag_ne": {
+    "category": "flags",
+    "moji": "🇳🇪",
+    "unicodeVersion": "6.0",
     "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
   },
-  {
-    "name": "flag_nf",
-    "unicode": "1F1F3-1F1EB",
+  "flag_nf": {
+    "category": "flags",
+    "moji": "🇳🇫",
+    "unicodeVersion": "6.0",
     "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
   },
-  {
-    "name": "nf",
-    "unicode": "1F1F3-1F1EB",
-    "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
-  },
-  {
-    "name": "flag_ng",
-    "unicode": "1F1F3-1F1EC",
-    "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
-  },
-  {
-    "name": "nigeria",
-    "unicode": "1F1F3-1F1EC",
+  "flag_ng": {
+    "category": "flags",
+    "moji": "🇳🇬",
+    "unicodeVersion": "6.0",
     "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
   },
-  {
-    "name": "flag_ni",
-    "unicode": "1F1F3-1F1EE",
+  "flag_ni": {
+    "category": "flags",
+    "moji": "🇳🇮",
+    "unicodeVersion": "6.0",
     "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
   },
-  {
-    "name": "ni",
-    "unicode": "1F1F3-1F1EE",
-    "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
-  },
-  {
-    "name": "flag_nl",
-    "unicode": "1F1F3-1F1F1",
+  "flag_nl": {
+    "category": "flags",
+    "moji": "🇳🇱",
+    "unicodeVersion": "6.0",
     "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
   },
-  {
-    "name": "nl",
-    "unicode": "1F1F3-1F1F1",
-    "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
-  },
-  {
-    "name": "flag_no",
-    "unicode": "1F1F3-1F1F4",
-    "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
-  },
-  {
-    "name": "no",
-    "unicode": "1F1F3-1F1F4",
+  "flag_no": {
+    "category": "flags",
+    "moji": "🇳🇴",
+    "unicodeVersion": "6.0",
     "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
   },
-  {
-    "name": "flag_np",
-    "unicode": "1F1F3-1F1F5",
-    "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
-  },
-  {
-    "name": "np",
-    "unicode": "1F1F3-1F1F5",
+  "flag_np": {
+    "category": "flags",
+    "moji": "🇳🇵",
+    "unicodeVersion": "6.0",
     "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
   },
-  {
-    "name": "flag_nr",
-    "unicode": "1F1F3-1F1F7",
-    "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
-  },
-  {
-    "name": "nr",
-    "unicode": "1F1F3-1F1F7",
+  "flag_nr": {
+    "category": "flags",
+    "moji": "🇳🇷",
+    "unicodeVersion": "6.0",
     "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
   },
-  {
-    "name": "flag_nu",
-    "unicode": "1F1F3-1F1FA",
+  "flag_nu": {
+    "category": "flags",
+    "moji": "🇳🇺",
+    "unicodeVersion": "6.0",
     "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
   },
-  {
-    "name": "nu",
-    "unicode": "1F1F3-1F1FA",
-    "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
-  },
-  {
-    "name": "flag_nz",
-    "unicode": "1F1F3-1F1FF",
+  "flag_nz": {
+    "category": "flags",
+    "moji": "🇳🇿",
+    "unicodeVersion": "6.0",
     "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
   },
-  {
-    "name": "nz",
-    "unicode": "1F1F3-1F1FF",
-    "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
-  },
-  {
-    "name": "flag_om",
-    "unicode": "1F1F4-1F1F2",
+  "flag_om": {
+    "category": "flags",
+    "moji": "🇴🇲",
+    "unicodeVersion": "6.0",
     "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
   },
-  {
-    "name": "om",
-    "unicode": "1F1F4-1F1F2",
-    "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
-  },
-  {
-    "name": "flag_pa",
-    "unicode": "1F1F5-1F1E6",
-    "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
-  },
-  {
-    "name": "pa",
-    "unicode": "1F1F5-1F1E6",
+  "flag_pa": {
+    "category": "flags",
+    "moji": "🇵🇦",
+    "unicodeVersion": "6.0",
     "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
   },
-  {
-    "name": "flag_pe",
-    "unicode": "1F1F5-1F1EA",
+  "flag_pe": {
+    "category": "flags",
+    "moji": "🇵🇪",
+    "unicodeVersion": "6.0",
     "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
   },
-  {
-    "name": "pe",
-    "unicode": "1F1F5-1F1EA",
-    "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
-  },
-  {
-    "name": "flag_pf",
-    "unicode": "1F1F5-1F1EB",
-    "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
-  },
-  {
-    "name": "pf",
-    "unicode": "1F1F5-1F1EB",
+  "flag_pf": {
+    "category": "flags",
+    "moji": "🇵🇫",
+    "unicodeVersion": "6.0",
     "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
   },
-  {
-    "name": "flag_pg",
-    "unicode": "1F1F5-1F1EC",
-    "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
-  },
-  {
-    "name": "pg",
-    "unicode": "1F1F5-1F1EC",
+  "flag_pg": {
+    "category": "flags",
+    "moji": "🇵🇬",
+    "unicodeVersion": "6.0",
     "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
   },
-  {
-    "name": "flag_ph",
-    "unicode": "1F1F5-1F1ED",
+  "flag_ph": {
+    "category": "flags",
+    "moji": "🇵🇭",
+    "unicodeVersion": "6.0",
     "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
   },
-  {
-    "name": "ph",
-    "unicode": "1F1F5-1F1ED",
-    "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
-  },
-  {
-    "name": "flag_pk",
-    "unicode": "1F1F5-1F1F0",
-    "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
-  },
-  {
-    "name": "pk",
-    "unicode": "1F1F5-1F1F0",
+  "flag_pk": {
+    "category": "flags",
+    "moji": "🇵🇰",
+    "unicodeVersion": "6.0",
     "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
   },
-  {
-    "name": "flag_pl",
-    "unicode": "1F1F5-1F1F1",
+  "flag_pl": {
+    "category": "flags",
+    "moji": "🇵🇱",
+    "unicodeVersion": "6.0",
     "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
   },
-  {
-    "name": "pl",
-    "unicode": "1F1F5-1F1F1",
-    "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
-  },
-  {
-    "name": "flag_pm",
-    "unicode": "1F1F5-1F1F2",
+  "flag_pm": {
+    "category": "flags",
+    "moji": "🇵🇲",
+    "unicodeVersion": "6.0",
     "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
   },
-  {
-    "name": "pm",
-    "unicode": "1F1F5-1F1F2",
-    "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
-  },
-  {
-    "name": "flag_pn",
-    "unicode": "1F1F5-1F1F3",
+  "flag_pn": {
+    "category": "flags",
+    "moji": "🇵🇳",
+    "unicodeVersion": "6.0",
     "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
   },
-  {
-    "name": "pn",
-    "unicode": "1F1F5-1F1F3",
-    "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
-  },
-  {
-    "name": "flag_pr",
-    "unicode": "1F1F5-1F1F7",
-    "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
-  },
-  {
-    "name": "pr",
-    "unicode": "1F1F5-1F1F7",
+  "flag_pr": {
+    "category": "flags",
+    "moji": "🇵🇷",
+    "unicodeVersion": "6.0",
     "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
   },
-  {
-    "name": "flag_ps",
-    "unicode": "1F1F5-1F1F8",
-    "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
-  },
-  {
-    "name": "ps",
-    "unicode": "1F1F5-1F1F8",
+  "flag_ps": {
+    "category": "flags",
+    "moji": "🇵🇸",
+    "unicodeVersion": "6.0",
     "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
   },
-  {
-    "name": "flag_pt",
-    "unicode": "1F1F5-1F1F9",
-    "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
-  },
-  {
-    "name": "pt",
-    "unicode": "1F1F5-1F1F9",
+  "flag_pt": {
+    "category": "flags",
+    "moji": "🇵🇹",
+    "unicodeVersion": "6.0",
     "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
   },
-  {
-    "name": "flag_pw",
-    "unicode": "1F1F5-1F1FC",
-    "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
-  },
-  {
-    "name": "pw",
-    "unicode": "1F1F5-1F1FC",
+  "flag_pw": {
+    "category": "flags",
+    "moji": "🇵🇼",
+    "unicodeVersion": "6.0",
     "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
   },
-  {
-    "name": "flag_py",
-    "unicode": "1F1F5-1F1FE",
+  "flag_py": {
+    "category": "flags",
+    "moji": "🇵🇾",
+    "unicodeVersion": "6.0",
     "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
   },
-  {
-    "name": "py",
-    "unicode": "1F1F5-1F1FE",
-    "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
-  },
-  {
-    "name": "flag_qa",
-    "unicode": "1F1F6-1F1E6",
+  "flag_qa": {
+    "category": "flags",
+    "moji": "🇶🇦",
+    "unicodeVersion": "6.0",
     "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
   },
-  {
-    "name": "qa",
-    "unicode": "1F1F6-1F1E6",
-    "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
-  },
-  {
-    "name": "flag_re",
-    "unicode": "1F1F7-1F1EA",
+  "flag_re": {
+    "category": "flags",
+    "moji": "🇷🇪",
+    "unicodeVersion": "6.0",
     "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
   },
-  {
-    "name": "re",
-    "unicode": "1F1F7-1F1EA",
-    "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
-  },
-  {
-    "name": "flag_ro",
-    "unicode": "1F1F7-1F1F4",
-    "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
-  },
-  {
-    "name": "ro",
-    "unicode": "1F1F7-1F1F4",
+  "flag_ro": {
+    "category": "flags",
+    "moji": "🇷🇴",
+    "unicodeVersion": "6.0",
     "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
   },
-  {
-    "name": "flag_rs",
-    "unicode": "1F1F7-1F1F8",
-    "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
-  },
-  {
-    "name": "rs",
-    "unicode": "1F1F7-1F1F8",
+  "flag_rs": {
+    "category": "flags",
+    "moji": "🇷🇸",
+    "unicodeVersion": "6.0",
     "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
   },
-  {
-    "name": "flag_ru",
-    "unicode": "1F1F7-1F1FA",
+  "flag_ru": {
+    "category": "flags",
+    "moji": "🇷🇺",
+    "unicodeVersion": "6.0",
     "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
   },
-  {
-    "name": "ru",
-    "unicode": "1F1F7-1F1FA",
-    "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
-  },
-  {
-    "name": "flag_rw",
-    "unicode": "1F1F7-1F1FC",
-    "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
-  },
-  {
-    "name": "rw",
-    "unicode": "1F1F7-1F1FC",
+  "flag_rw": {
+    "category": "flags",
+    "moji": "🇷🇼",
+    "unicodeVersion": "6.0",
     "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
   },
-  {
-    "name": "flag_sa",
-    "unicode": "1F1F8-1F1E6",
+  "flag_sa": {
+    "category": "flags",
+    "moji": "🇸🇦",
+    "unicodeVersion": "6.0",
     "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
   },
-  {
-    "name": "saudiarabia",
-    "unicode": "1F1F8-1F1E6",
-    "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
-  },
-  {
-    "name": "saudi",
-    "unicode": "1F1F8-1F1E6",
-    "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
-  },
-  {
-    "name": "flag_sb",
-    "unicode": "1F1F8-1F1E7",
+  "flag_sb": {
+    "category": "flags",
+    "moji": "🇸🇧",
+    "unicodeVersion": "6.0",
     "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
   },
-  {
-    "name": "sb",
-    "unicode": "1F1F8-1F1E7",
-    "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
-  },
-  {
-    "name": "flag_sc",
-    "unicode": "1F1F8-1F1E8",
-    "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
-  },
-  {
-    "name": "sc",
-    "unicode": "1F1F8-1F1E8",
+  "flag_sc": {
+    "category": "flags",
+    "moji": "🇸🇨",
+    "unicodeVersion": "6.0",
     "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
   },
-  {
-    "name": "flag_sd",
-    "unicode": "1F1F8-1F1E9",
-    "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
-  },
-  {
-    "name": "sd",
-    "unicode": "1F1F8-1F1E9",
+  "flag_sd": {
+    "category": "flags",
+    "moji": "🇸🇩",
+    "unicodeVersion": "6.0",
     "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
   },
-  {
-    "name": "flag_se",
-    "unicode": "1F1F8-1F1EA",
-    "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
-  },
-  {
-    "name": "se",
-    "unicode": "1F1F8-1F1EA",
+  "flag_se": {
+    "category": "flags",
+    "moji": "🇸🇪",
+    "unicodeVersion": "6.0",
     "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
   },
-  {
-    "name": "flag_sg",
-    "unicode": "1F1F8-1F1EC",
-    "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
-  },
-  {
-    "name": "sg",
-    "unicode": "1F1F8-1F1EC",
+  "flag_sg": {
+    "category": "flags",
+    "moji": "🇸🇬",
+    "unicodeVersion": "6.0",
     "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
   },
-  {
-    "name": "flag_sh",
-    "unicode": "1F1F8-1F1ED",
+  "flag_sh": {
+    "category": "flags",
+    "moji": "🇸🇭",
+    "unicodeVersion": "6.0",
     "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
   },
-  {
-    "name": "sh",
-    "unicode": "1F1F8-1F1ED",
-    "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
-  },
-  {
-    "name": "flag_si",
-    "unicode": "1F1F8-1F1EE",
-    "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
-  },
-  {
-    "name": "si",
-    "unicode": "1F1F8-1F1EE",
+  "flag_si": {
+    "category": "flags",
+    "moji": "🇸🇮",
+    "unicodeVersion": "6.0",
     "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
   },
-  {
-    "name": "flag_sj",
-    "unicode": "1F1F8-1F1EF",
+  "flag_sj": {
+    "category": "flags",
+    "moji": "🇸🇯",
+    "unicodeVersion": "6.0",
     "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
   },
-  {
-    "name": "sj",
-    "unicode": "1F1F8-1F1EF",
-    "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
-  },
-  {
-    "name": "flag_sk",
-    "unicode": "1F1F8-1F1F0",
-    "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
-  },
-  {
-    "name": "sk",
-    "unicode": "1F1F8-1F1F0",
+  "flag_sk": {
+    "category": "flags",
+    "moji": "🇸🇰",
+    "unicodeVersion": "6.0",
     "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
   },
-  {
-    "name": "flag_sl",
-    "unicode": "1F1F8-1F1F1",
+  "flag_sl": {
+    "category": "flags",
+    "moji": "🇸🇱",
+    "unicodeVersion": "6.0",
     "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
   },
-  {
-    "name": "sl",
-    "unicode": "1F1F8-1F1F1",
-    "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
-  },
-  {
-    "name": "flag_sm",
-    "unicode": "1F1F8-1F1F2",
-    "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
-  },
-  {
-    "name": "sm",
-    "unicode": "1F1F8-1F1F2",
+  "flag_sm": {
+    "category": "flags",
+    "moji": "🇸🇲",
+    "unicodeVersion": "6.0",
     "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
   },
-  {
-    "name": "flag_sn",
-    "unicode": "1F1F8-1F1F3",
+  "flag_sn": {
+    "category": "flags",
+    "moji": "🇸🇳",
+    "unicodeVersion": "6.0",
     "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
   },
-  {
-    "name": "sn",
-    "unicode": "1F1F8-1F1F3",
-    "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
-  },
-  {
-    "name": "flag_so",
-    "unicode": "1F1F8-1F1F4",
-    "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
-  },
-  {
-    "name": "so",
-    "unicode": "1F1F8-1F1F4",
+  "flag_so": {
+    "category": "flags",
+    "moji": "🇸🇴",
+    "unicodeVersion": "6.0",
     "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
   },
-  {
-    "name": "flag_sr",
-    "unicode": "1F1F8-1F1F7",
+  "flag_sr": {
+    "category": "flags",
+    "moji": "🇸🇷",
+    "unicodeVersion": "6.0",
     "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
   },
-  {
-    "name": "sr",
-    "unicode": "1F1F8-1F1F7",
-    "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
-  },
-  {
-    "name": "flag_ss",
-    "unicode": "1F1F8-1F1F8",
-    "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
-  },
-  {
-    "name": "ss",
-    "unicode": "1F1F8-1F1F8",
+  "flag_ss": {
+    "category": "flags",
+    "moji": "🇸🇸",
+    "unicodeVersion": "6.0",
     "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
   },
-  {
-    "name": "flag_st",
-    "unicode": "1F1F8-1F1F9",
+  "flag_st": {
+    "category": "flags",
+    "moji": "🇸🇹",
+    "unicodeVersion": "6.0",
     "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
   },
-  {
-    "name": "st",
-    "unicode": "1F1F8-1F1F9",
-    "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
-  },
-  {
-    "name": "flag_sv",
-    "unicode": "1F1F8-1F1FB",
-    "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
-  },
-  {
-    "name": "sv",
-    "unicode": "1F1F8-1F1FB",
+  "flag_sv": {
+    "category": "flags",
+    "moji": "🇸🇻",
+    "unicodeVersion": "6.0",
     "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
   },
-  {
-    "name": "flag_sx",
-    "unicode": "1F1F8-1F1FD",
+  "flag_sx": {
+    "category": "flags",
+    "moji": "🇸🇽",
+    "unicodeVersion": "6.0",
     "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
   },
-  {
-    "name": "sx",
-    "unicode": "1F1F8-1F1FD",
-    "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
-  },
-  {
-    "name": "flag_sy",
-    "unicode": "1F1F8-1F1FE",
-    "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
-  },
-  {
-    "name": "sy",
-    "unicode": "1F1F8-1F1FE",
+  "flag_sy": {
+    "category": "flags",
+    "moji": "🇸🇾",
+    "unicodeVersion": "6.0",
     "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
   },
-  {
-    "name": "flag_sz",
-    "unicode": "1F1F8-1F1FF",
+  "flag_sz": {
+    "category": "flags",
+    "moji": "🇸🇿",
+    "unicodeVersion": "6.0",
     "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
   },
-  {
-    "name": "sz",
-    "unicode": "1F1F8-1F1FF",
-    "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
-  },
-  {
-    "name": "flag_ta",
-    "unicode": "1F1F9-1F1E6",
-    "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
-  },
-  {
-    "name": "ta",
-    "unicode": "1F1F9-1F1E6",
+  "flag_ta": {
+    "category": "flags",
+    "moji": "🇹🇦",
+    "unicodeVersion": "6.0",
     "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
   },
-  {
-    "name": "flag_tc",
-    "unicode": "1F1F9-1F1E8",
+  "flag_tc": {
+    "category": "flags",
+    "moji": "🇹🇨",
+    "unicodeVersion": "6.0",
     "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
   },
-  {
-    "name": "tc",
-    "unicode": "1F1F9-1F1E8",
-    "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
-  },
-  {
-    "name": "flag_td",
-    "unicode": "1F1F9-1F1E9",
-    "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
-  },
-  {
-    "name": "td",
-    "unicode": "1F1F9-1F1E9",
+  "flag_td": {
+    "category": "flags",
+    "moji": "🇹🇩",
+    "unicodeVersion": "6.0",
     "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
   },
-  {
-    "name": "flag_tf",
-    "unicode": "1F1F9-1F1EB",
+  "flag_tf": {
+    "category": "flags",
+    "moji": "🇹🇫",
+    "unicodeVersion": "6.0",
     "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
   },
-  {
-    "name": "tf",
-    "unicode": "1F1F9-1F1EB",
-    "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
-  },
-  {
-    "name": "flag_tg",
-    "unicode": "1F1F9-1F1EC",
-    "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
-  },
-  {
-    "name": "tg",
-    "unicode": "1F1F9-1F1EC",
+  "flag_tg": {
+    "category": "flags",
+    "moji": "🇹🇬",
+    "unicodeVersion": "6.0",
     "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
   },
-  {
-    "name": "flag_th",
-    "unicode": "1F1F9-1F1ED",
+  "flag_th": {
+    "category": "flags",
+    "moji": "🇹🇭",
+    "unicodeVersion": "6.0",
     "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
   },
-  {
-    "name": "th",
-    "unicode": "1F1F9-1F1ED",
-    "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
-  },
-  {
-    "name": "flag_tj",
-    "unicode": "1F1F9-1F1EF",
-    "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
-  },
-  {
-    "name": "tj",
-    "unicode": "1F1F9-1F1EF",
+  "flag_tj": {
+    "category": "flags",
+    "moji": "🇹🇯",
+    "unicodeVersion": "6.0",
     "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
   },
-  {
-    "name": "flag_tk",
-    "unicode": "1F1F9-1F1F0",
+  "flag_tk": {
+    "category": "flags",
+    "moji": "🇹🇰",
+    "unicodeVersion": "6.0",
     "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
   },
-  {
-    "name": "tk",
-    "unicode": "1F1F9-1F1F0",
-    "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
-  },
-  {
-    "name": "flag_tl",
-    "unicode": "1F1F9-1F1F1",
-    "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
-  },
-  {
-    "name": "tl",
-    "unicode": "1F1F9-1F1F1",
+  "flag_tl": {
+    "category": "flags",
+    "moji": "🇹🇱",
+    "unicodeVersion": "6.0",
     "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
   },
-  {
-    "name": "flag_tm",
-    "unicode": "1F1F9-1F1F2",
+  "flag_tm": {
+    "category": "flags",
+    "moji": "🇹🇲",
+    "unicodeVersion": "6.0",
     "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
   },
-  {
-    "name": "turkmenistan",
-    "unicode": "1F1F9-1F1F2",
-    "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
-  },
-  {
-    "name": "flag_tn",
-    "unicode": "1F1F9-1F1F3",
-    "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
-  },
-  {
-    "name": "tn",
-    "unicode": "1F1F9-1F1F3",
+  "flag_tn": {
+    "category": "flags",
+    "moji": "🇹🇳",
+    "unicodeVersion": "6.0",
     "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
   },
-  {
-    "name": "flag_to",
-    "unicode": "1F1F9-1F1F4",
+  "flag_to": {
+    "category": "flags",
+    "moji": "🇹🇴",
+    "unicodeVersion": "6.0",
     "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
   },
-  {
-    "name": "to",
-    "unicode": "1F1F9-1F1F4",
-    "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
-  },
-  {
-    "name": "flag_tr",
-    "unicode": "1F1F9-1F1F7",
-    "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
-  },
-  {
-    "name": "tr",
-    "unicode": "1F1F9-1F1F7",
+  "flag_tr": {
+    "category": "flags",
+    "moji": "🇹🇷",
+    "unicodeVersion": "6.0",
     "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
   },
-  {
-    "name": "flag_tt",
-    "unicode": "1F1F9-1F1F9",
+  "flag_tt": {
+    "category": "flags",
+    "moji": "🇹🇹",
+    "unicodeVersion": "6.0",
     "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
   },
-  {
-    "name": "tt",
-    "unicode": "1F1F9-1F1F9",
-    "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
-  },
-  {
-    "name": "flag_tv",
-    "unicode": "1F1F9-1F1FB",
-    "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
-  },
-  {
-    "name": "tuvalu",
-    "unicode": "1F1F9-1F1FB",
+  "flag_tv": {
+    "category": "flags",
+    "moji": "🇹🇻",
+    "unicodeVersion": "6.0",
     "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
   },
-  {
-    "name": "flag_tw",
-    "unicode": "1F1F9-1F1FC",
+  "flag_tw": {
+    "category": "flags",
+    "moji": "🇹🇼",
+    "unicodeVersion": "6.0",
     "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
   },
-  {
-    "name": "tw",
-    "unicode": "1F1F9-1F1FC",
-    "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
-  },
-  {
-    "name": "flag_tz",
-    "unicode": "1F1F9-1F1FF",
-    "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
-  },
-  {
-    "name": "tz",
-    "unicode": "1F1F9-1F1FF",
+  "flag_tz": {
+    "category": "flags",
+    "moji": "🇹🇿",
+    "unicodeVersion": "6.0",
     "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
   },
-  {
-    "name": "flag_ua",
-    "unicode": "1F1FA-1F1E6",
+  "flag_ua": {
+    "category": "flags",
+    "moji": "🇺🇦",
+    "unicodeVersion": "6.0",
     "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
   },
-  {
-    "name": "ua",
-    "unicode": "1F1FA-1F1E6",
-    "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
-  },
-  {
-    "name": "flag_ug",
-    "unicode": "1F1FA-1F1EC",
-    "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
-  },
-  {
-    "name": "ug",
-    "unicode": "1F1FA-1F1EC",
+  "flag_ug": {
+    "category": "flags",
+    "moji": "🇺🇬",
+    "unicodeVersion": "6.0",
     "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
   },
-  {
-    "name": "flag_um",
-    "unicode": "1F1FA-1F1F2",
+  "flag_um": {
+    "category": "flags",
+    "moji": "🇺🇲",
+    "unicodeVersion": "6.0",
     "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
   },
-  {
-    "name": "um",
-    "unicode": "1F1FA-1F1F2",
-    "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
-  },
-  {
-    "name": "flag_us",
-    "unicode": "1F1FA-1F1F8",
-    "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
-  },
-  {
-    "name": "us",
-    "unicode": "1F1FA-1F1F8",
+  "flag_us": {
+    "category": "flags",
+    "moji": "🇺🇸",
+    "unicodeVersion": "6.0",
     "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
   },
-  {
-    "name": "flag_uy",
-    "unicode": "1F1FA-1F1FE",
+  "flag_uy": {
+    "category": "flags",
+    "moji": "🇺🇾",
+    "unicodeVersion": "6.0",
     "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
   },
-  {
-    "name": "uy",
-    "unicode": "1F1FA-1F1FE",
-    "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
-  },
-  {
-    "name": "flag_uz",
-    "unicode": "1F1FA-1F1FF",
-    "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
-  },
-  {
-    "name": "uz",
-    "unicode": "1F1FA-1F1FF",
+  "flag_uz": {
+    "category": "flags",
+    "moji": "🇺🇿",
+    "unicodeVersion": "6.0",
     "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
   },
-  {
-    "name": "flag_va",
-    "unicode": "1F1FB-1F1E6",
+  "flag_va": {
+    "category": "flags",
+    "moji": "🇻🇦",
+    "unicodeVersion": "6.0",
     "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
   },
-  {
-    "name": "va",
-    "unicode": "1F1FB-1F1E6",
-    "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
-  },
-  {
-    "name": "flag_vc",
-    "unicode": "1F1FB-1F1E8",
-    "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
-  },
-  {
-    "name": "vc",
-    "unicode": "1F1FB-1F1E8",
+  "flag_vc": {
+    "category": "flags",
+    "moji": "🇻🇨",
+    "unicodeVersion": "6.0",
     "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
   },
-  {
-    "name": "flag_ve",
-    "unicode": "1F1FB-1F1EA",
+  "flag_ve": {
+    "category": "flags",
+    "moji": "🇻🇪",
+    "unicodeVersion": "6.0",
     "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
   },
-  {
-    "name": "ve",
-    "unicode": "1F1FB-1F1EA",
-    "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
-  },
-  {
-    "name": "flag_vg",
-    "unicode": "1F1FB-1F1EC",
-    "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
-  },
-  {
-    "name": "vg",
-    "unicode": "1F1FB-1F1EC",
+  "flag_vg": {
+    "category": "flags",
+    "moji": "🇻🇬",
+    "unicodeVersion": "6.0",
     "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
   },
-  {
-    "name": "flag_vi",
-    "unicode": "1F1FB-1F1EE",
+  "flag_vi": {
+    "category": "flags",
+    "moji": "🇻🇮",
+    "unicodeVersion": "6.0",
     "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
   },
-  {
-    "name": "vi",
-    "unicode": "1F1FB-1F1EE",
-    "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
-  },
-  {
-    "name": "flag_vn",
-    "unicode": "1F1FB-1F1F3",
-    "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
-  },
-  {
-    "name": "vn",
-    "unicode": "1F1FB-1F1F3",
+  "flag_vn": {
+    "category": "flags",
+    "moji": "🇻🇳",
+    "unicodeVersion": "6.0",
     "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
   },
-  {
-    "name": "flag_vu",
-    "unicode": "1F1FB-1F1FA",
+  "flag_vu": {
+    "category": "flags",
+    "moji": "🇻🇺",
+    "unicodeVersion": "6.0",
     "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
   },
-  {
-    "name": "vu",
-    "unicode": "1F1FB-1F1FA",
-    "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
-  },
-  {
-    "name": "flag_wf",
-    "unicode": "1F1FC-1F1EB",
-    "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
-  },
-  {
-    "name": "wf",
-    "unicode": "1F1FC-1F1EB",
+  "flag_wf": {
+    "category": "flags",
+    "moji": "🇼🇫",
+    "unicodeVersion": "6.0",
     "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
   },
-  {
-    "name": "flag_white",
-    "unicode": "1F3F3",
+  "flag_white": {
+    "category": "objects",
+    "moji": "🏳",
+    "unicodeVersion": "6.0",
     "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
   },
-  {
-    "name": "waving_white_flag",
-    "unicode": "1F3F3",
-    "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
-  },
-  {
-    "name": "flag_ws",
-    "unicode": "1F1FC-1F1F8",
-    "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
-  },
-  {
-    "name": "ws",
-    "unicode": "1F1FC-1F1F8",
+  "flag_ws": {
+    "category": "flags",
+    "moji": "🇼🇸",
+    "unicodeVersion": "6.0",
     "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
   },
-  {
-    "name": "flag_xk",
-    "unicode": "1F1FD-1F1F0",
+  "flag_xk": {
+    "category": "flags",
+    "moji": "🇽🇰",
+    "unicodeVersion": "6.0",
     "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
   },
-  {
-    "name": "xk",
-    "unicode": "1F1FD-1F1F0",
-    "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
-  },
-  {
-    "name": "flag_ye",
-    "unicode": "1F1FE-1F1EA",
-    "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
-  },
-  {
-    "name": "ye",
-    "unicode": "1F1FE-1F1EA",
+  "flag_ye": {
+    "category": "flags",
+    "moji": "🇾🇪",
+    "unicodeVersion": "6.0",
     "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
   },
-  {
-    "name": "flag_yt",
-    "unicode": "1F1FE-1F1F9",
+  "flag_yt": {
+    "category": "flags",
+    "moji": "🇾🇹",
+    "unicodeVersion": "6.0",
     "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
   },
-  {
-    "name": "yt",
-    "unicode": "1F1FE-1F1F9",
-    "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
-  },
-  {
-    "name": "flag_za",
-    "unicode": "1F1FF-1F1E6",
-    "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
-  },
-  {
-    "name": "za",
-    "unicode": "1F1FF-1F1E6",
+  "flag_za": {
+    "category": "flags",
+    "moji": "🇿🇦",
+    "unicodeVersion": "6.0",
     "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
   },
-  {
-    "name": "flag_zm",
-    "unicode": "1F1FF-1F1F2",
+  "flag_zm": {
+    "category": "flags",
+    "moji": "🇿🇲",
+    "unicodeVersion": "6.0",
     "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
   },
-  {
-    "name": "zm",
-    "unicode": "1F1FF-1F1F2",
-    "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
-  },
-  {
-    "name": "flag_zw",
-    "unicode": "1F1FF-1F1FC",
-    "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
-  },
-  {
-    "name": "zw",
-    "unicode": "1F1FF-1F1FC",
+  "flag_zw": {
+    "category": "flags",
+    "moji": "🇿🇼",
+    "unicodeVersion": "6.0",
     "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
   },
-  {
-    "name": "flags",
-    "unicode": "1F38F",
+  "flags": {
+    "category": "objects",
+    "moji": "🎏",
+    "unicodeVersion": "6.0",
     "digest": "f860aa4df587cf140c3e9735bbd101e9fd5a1bfcea42e420d85ac0a9877fa21d"
   },
-  {
-    "name": "flashlight",
-    "unicode": "1F526",
+  "flashlight": {
+    "category": "objects",
+    "moji": "🔦",
+    "unicodeVersion": "6.0",
     "digest": "e929bbe76e0fd2dc5bd6476858a0bbc717fd21467710435d35d80efb38033d73"
   },
-  {
-    "name": "fleur-de-lis",
-    "unicode": "269C",
+  "fleur-de-lis": {
+    "category": "symbols",
+    "moji": "⚜",
+    "unicodeVersion": "4.1",
     "digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3"
   },
-  {
-    "name": "floppy_disk",
-    "unicode": "1F4BE",
+  "floppy_disk": {
+    "category": "objects",
+    "moji": "💾",
+    "unicodeVersion": "6.0",
     "digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0"
   },
-  {
-    "name": "flower_playing_cards",
-    "unicode": "1F3B4",
+  "flower_playing_cards": {
+    "category": "symbols",
+    "moji": "🎴",
+    "unicodeVersion": "6.0",
     "digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869"
   },
-  {
-    "name": "flushed",
-    "unicode": "1F633",
+  "flushed": {
+    "category": "people",
+    "moji": "😳",
+    "unicodeVersion": "6.0",
     "digest": "e759d46bab92af5494d78b6c712c06568759afe397e7828ca0a0de1e3eab0165"
   },
-  {
-    "name": "fog",
-    "unicode": "1F32B",
+  "fog": {
+    "category": "nature",
+    "moji": "🌫",
+    "unicodeVersion": "7.0",
     "digest": "0cbd4733961d30fe0f40f95dd1f37254aebbef26f82dd18ad2000e799eb2898e"
   },
-  {
-    "name": "foggy",
-    "unicode": "1F301",
+  "foggy": {
+    "category": "travel",
+    "moji": "🌁",
+    "unicodeVersion": "6.0",
     "digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca"
   },
-  {
-    "name": "football",
-    "unicode": "1F3C8",
+  "football": {
+    "category": "activity",
+    "moji": "🏈",
+    "unicodeVersion": "6.0",
     "digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c"
   },
-  {
-    "name": "footprints",
-    "unicode": "1F463",
+  "footprints": {
+    "category": "people",
+    "moji": "👣",
+    "unicodeVersion": "6.0",
     "digest": "85bbf2bc0ae8e6259d83a06f513600095d7fcfc44372670f5b2405d380b78811"
   },
-  {
-    "name": "fork_and_knife",
-    "unicode": "1F374",
+  "fork_and_knife": {
+    "category": "food",
+    "moji": "🍴",
+    "unicodeVersion": "6.0",
     "digest": "f228accd36ddccb4ec636207c19d7185191ec79723b780a1bd5c3d00a4b1ef3b"
   },
-  {
-    "name": "fork_knife_plate",
-    "unicode": "1F37D",
+  "fork_knife_plate": {
+    "category": "food",
+    "moji": "🍽",
+    "unicodeVersion": "7.0",
     "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
   },
-  {
-    "name": "fork_and_knife_with_plate",
-    "unicode": "1F37D",
-    "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
-  },
-  {
-    "name": "fountain",
-    "unicode": "26F2",
+  "fountain": {
+    "category": "travel",
+    "moji": "⛲",
+    "unicodeVersion": "5.2",
     "digest": "87043f9256e1d4615159307fcfd21bf6ae2aba0bada7de2bd50d7d6f2ab82395"
   },
-  {
-    "name": "four",
-    "unicode": "0034-20E3",
+  "four": {
+    "category": "symbols",
+    "moji": "4️⃣",
+    "unicodeVersion": "3.0",
     "digest": "c2c82a966bbb599aae557d930a4fc42604f2081aa45528872f5caf4942ee79d9"
   },
-  {
-    "name": "four_leaf_clover",
-    "unicode": "1F340",
+  "four_leaf_clover": {
+    "category": "nature",
+    "moji": "🍀",
+    "unicodeVersion": "6.0",
     "digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8"
   },
-  {
-    "name": "fox",
-    "unicode": "1F98A",
-    "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
-  },
-  {
-    "name": "fox_face",
-    "unicode": "1F98A",
+  "fox": {
+    "category": "nature",
+    "moji": "🦊",
+    "unicodeVersion": "9.0",
     "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
   },
-  {
-    "name": "frame_photo",
-    "unicode": "1F5BC",
+  "frame_photo": {
+    "category": "objects",
+    "moji": "🖼",
+    "unicodeVersion": "7.0",
     "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
   },
-  {
-    "name": "frame_with_picture",
-    "unicode": "1F5BC",
-    "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
-  },
-  {
-    "name": "free",
-    "unicode": "1F193",
+  "free": {
+    "category": "symbols",
+    "moji": "🆓",
+    "unicodeVersion": "6.0",
     "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa"
   },
-  {
-    "name": "french_bread",
-    "unicode": "1F956",
-    "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
-  },
-  {
-    "name": "baguette_bread",
-    "unicode": "1F956",
+  "french_bread": {
+    "category": "food",
+    "moji": "🥖",
+    "unicodeVersion": "9.0",
     "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
   },
-  {
-    "name": "fried_shrimp",
-    "unicode": "1F364",
+  "fried_shrimp": {
+    "category": "food",
+    "moji": "🍤",
+    "unicodeVersion": "6.0",
     "digest": "0792bdc4484852de970c8f43bc3a1a339dc0e48090ec77d6de97cbfcdd17f9e1"
   },
-  {
-    "name": "fries",
-    "unicode": "1F35F",
+  "fries": {
+    "category": "food",
+    "moji": "🍟",
+    "unicodeVersion": "6.0",
     "digest": "47915aea67251d358d91a0e4dc3dcc347155336007d6b931a192be72a743b4e9"
   },
-  {
-    "name": "frog",
-    "unicode": "1F438",
+  "frog": {
+    "category": "nature",
+    "moji": "🐸",
+    "unicodeVersion": "6.0",
     "digest": "d024b2ce771df64040534fb0906737d18b562bc3578dee62c2f25ec03c7caffd"
   },
-  {
-    "name": "frowning",
-    "unicode": "1F626",
+  "frowning": {
+    "category": "people",
+    "moji": "😦",
+    "unicodeVersion": "6.1",
     "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
   },
-  {
-    "name": "anguished",
-    "unicode": "1F626",
-    "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
-  },
-  {
-    "name": "frowning2",
-    "unicode": "2639",
-    "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
-  },
-  {
-    "name": "white_frowning_face",
-    "unicode": "2639",
+  "frowning2": {
+    "category": "people",
+    "moji": "☹",
+    "unicodeVersion": "1.1",
     "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
   },
-  {
-    "name": "fuelpump",
-    "unicode": "26FD",
+  "fuelpump": {
+    "category": "travel",
+    "moji": "⛽",
+    "unicodeVersion": "5.2",
     "digest": "105e736469f19911b8bab4ab6d29f949ded4b061b54e3dd763726577d6453095"
   },
-  {
-    "name": "full_moon",
-    "unicode": "1F315",
+  "full_moon": {
+    "category": "nature",
+    "moji": "🌕",
+    "unicodeVersion": "6.0",
     "digest": "aaa87f4676a5aaa29c1b721a3b582e89db6c1f35a25c52e4b480bd193ef39c43"
   },
-  {
-    "name": "full_moon_with_face",
-    "unicode": "1F31D",
+  "full_moon_with_face": {
+    "category": "nature",
+    "moji": "🌝",
+    "unicodeVersion": "6.0",
     "digest": "05c4b9c339fcdf81ae67027641522baa99c370d87873ff4c8133b8349e627e33"
   },
-  {
-    "name": "game_die",
-    "unicode": "1F3B2",
+  "game_die": {
+    "category": "activity",
+    "moji": "🎲",
+    "unicodeVersion": "6.0",
     "digest": "00d19ce8e21dba2cdfeb18709fa8741f3af9d6207f81d5657b68e05e64f105a8"
   },
-  {
-    "name": "gear",
-    "unicode": "2699",
+  "gear": {
+    "category": "objects",
+    "moji": "⚙",
+    "unicodeVersion": "4.1",
     "digest": "c5ba354c0f7a36dce95477091984e352ecc59af8c9f26a94ad8e296dc042b9de"
   },
-  {
-    "name": "gem",
-    "unicode": "1F48E",
+  "gem": {
+    "category": "objects",
+    "moji": "💎",
+    "unicodeVersion": "6.0",
     "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
   },
-  {
-    "name": "gemini",
-    "unicode": "264A",
+  "gemini": {
+    "category": "symbols",
+    "moji": "♊",
+    "unicodeVersion": "1.1",
     "digest": "278239c598d490a110f1f3f52fc3b85259be8e76034b38228ef3f68d7ddd8cdd"
   },
-  {
-    "name": "ghost",
-    "unicode": "1F47B",
+  "ghost": {
+    "category": "people",
+    "moji": "👻",
+    "unicodeVersion": "6.0",
     "digest": "80d528fcf8ef9198631527547e43a608a4332a799f9e5550b8318dec67c9c4d2"
   },
-  {
-    "name": "gift",
-    "unicode": "1F381",
+  "gift": {
+    "category": "objects",
+    "moji": "🎁",
+    "unicodeVersion": "6.0",
     "digest": "4061a84a59f0300473299678c43e533341eb965db09597fffc6e221fd7b77376"
   },
-  {
-    "name": "gift_heart",
-    "unicode": "1F49D",
+  "gift_heart": {
+    "category": "symbols",
+    "moji": "💝",
+    "unicodeVersion": "6.0",
     "digest": "5420199b515b9b32c964a3c19d87e07461639e3068a939dae26c6436335c0cee"
   },
-  {
-    "name": "girl",
-    "unicode": "1F467",
+  "girl": {
+    "category": "people",
+    "moji": "👧",
+    "unicodeVersion": "6.0",
     "digest": "8d2d0b72a91e6e44921b71030ffc4c89c0f50f1364787784afe1e7e568cf1bc6"
   },
-  {
-    "name": "girl_tone1",
-    "unicode": "1F467-1F3FB",
+  "girl_tone1": {
+    "category": "people",
+    "moji": "👧🏻",
+    "unicodeVersion": "8.0",
     "digest": "bda12a6b38994a578ee65166bbdd93ea04df4101697b52ed236de8d687df09de"
   },
-  {
-    "name": "girl_tone2",
-    "unicode": "1F467-1F3FC",
+  "girl_tone2": {
+    "category": "people",
+    "moji": "👧🏼",
+    "unicodeVersion": "8.0",
     "digest": "de7a0925c30b7181a289f71b1a849c1b7751ee8c104e8f2029bd9c2fe3f91c64"
   },
-  {
-    "name": "girl_tone3",
-    "unicode": "1F467-1F3FD",
+  "girl_tone3": {
+    "category": "people",
+    "moji": "👧🏽",
+    "unicodeVersion": "8.0",
     "digest": "e41272816db0e642d003dce7cb262e1593a592251f46729f7830f4515149e1f2"
   },
-  {
-    "name": "girl_tone4",
-    "unicode": "1F467-1F3FE",
+  "girl_tone4": {
+    "category": "people",
+    "moji": "👧🏾",
+    "unicodeVersion": "8.0",
     "digest": "8d6a4513ecbf08408c0ecc5336767777a2216f7a19437faf9e51f65101822469"
   },
-  {
-    "name": "girl_tone5",
-    "unicode": "1F467-1F3FF",
+  "girl_tone5": {
+    "category": "people",
+    "moji": "👧🏿",
+    "unicodeVersion": "8.0",
     "digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d"
   },
-  {
-    "name": "globe_with_meridians",
-    "unicode": "1F310",
+  "globe_with_meridians": {
+    "category": "symbols",
+    "moji": "🌐",
+    "unicodeVersion": "6.0",
     "digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75"
   },
-  {
-    "name": "goal",
-    "unicode": "1F945",
+  "goal": {
+    "category": "activity",
+    "moji": "🥅",
+    "unicodeVersion": "9.0",
     "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
   },
-  {
-    "name": "goal_net",
-    "unicode": "1F945",
-    "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
-  },
-  {
-    "name": "goat",
-    "unicode": "1F410",
+  "goat": {
+    "category": "nature",
+    "moji": "🐐",
+    "unicodeVersion": "6.0",
     "digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8"
   },
-  {
-    "name": "golf",
-    "unicode": "26F3",
+  "golf": {
+    "category": "activity",
+    "moji": "⛳",
+    "unicodeVersion": "5.2",
     "digest": "eed79364754eec97855e3c7b584f347ae139d9ddb4eb7fb66c00867610b8f1c1"
   },
-  {
-    "name": "golfer",
-    "unicode": "1F3CC",
+  "golfer": {
+    "category": "activity",
+    "moji": "🏌",
+    "unicodeVersion": "7.0",
     "digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344"
   },
-  {
-    "name": "gorilla",
-    "unicode": "1F98D",
+  "gorilla": {
+    "category": "nature",
+    "moji": "🦍",
+    "unicodeVersion": "9.0",
     "digest": "4a564dc14f8ae5450d094f6410ec7f099a7f07dc5254b6395f44a35527bdb4b7"
   },
-  {
-    "name": "grapes",
-    "unicode": "1F347",
+  "grapes": {
+    "category": "food",
+    "moji": "🍇",
+    "unicodeVersion": "6.0",
     "digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50"
   },
-  {
-    "name": "green_apple",
-    "unicode": "1F34F",
+  "green_apple": {
+    "category": "food",
+    "moji": "🍏",
+    "unicodeVersion": "6.0",
     "digest": "457490e9b2b20894f50768262d63f1021717079da104d4847076b3fa779e9a21"
   },
-  {
-    "name": "green_book",
-    "unicode": "1F4D7",
+  "green_book": {
+    "category": "objects",
+    "moji": "📗",
+    "unicodeVersion": "6.0",
     "digest": "370f635b200efe5e4a9f17da58bd22500e258e61d17795cef375f19c9a45468f"
   },
-  {
-    "name": "green_heart",
-    "unicode": "1F49A",
+  "green_heart": {
+    "category": "symbols",
+    "moji": "💚",
+    "unicodeVersion": "6.0",
     "digest": "f71e30416d9019873f2ed38ef375c48386424ff60b5a07b89b15dc9e0a3970f9"
   },
-  {
-    "name": "grey_exclamation",
-    "unicode": "2755",
+  "grey_exclamation": {
+    "category": "symbols",
+    "moji": "❕",
+    "unicodeVersion": "6.0",
     "digest": "2fa1d356e12c17cc4025e43afb6c3070385f677102a35223302fda46c47a9b03"
   },
-  {
-    "name": "grey_question",
-    "unicode": "2754",
+  "grey_question": {
+    "category": "symbols",
+    "moji": "❔",
+    "unicodeVersion": "6.0",
     "digest": "e1035bcbf0f66d238ef478ba451f5cf2c51627fbf101ed03bad3b2bf38db8aa2"
   },
-  {
-    "name": "grimacing",
-    "unicode": "1F62C",
+  "grimacing": {
+    "category": "people",
+    "moji": "😬",
+    "unicodeVersion": "6.1",
     "digest": "2cedad13b8b2a1d4385ca6fa88a251eb7757a4c65dd6d362267864a01247846b"
   },
-  {
-    "name": "grin",
-    "unicode": "1F601",
+  "grin": {
+    "category": "people",
+    "moji": "😁",
+    "unicodeVersion": "6.0",
     "digest": "634b2f37e32e57ed6edc7f371993a92e34137dd21ba393de5227cfbbe2422815"
   },
-  {
-    "name": "grinning",
-    "unicode": "1F600",
+  "grinning": {
+    "category": "people",
+    "moji": "😀",
+    "unicodeVersion": "6.1",
     "digest": "cef76aa41771db9fd1d6bd9b4233c22c1fb1931494af54cab29e6347ed9b678d"
   },
-  {
-    "name": "guardsman",
-    "unicode": "1F482",
+  "guardsman": {
+    "category": "people",
+    "moji": "💂",
+    "unicodeVersion": "6.0",
     "digest": "17bc7fad6b8c8dbd015bb709380d129f8b8e1e971062d15e6ab0b2e63e500564"
   },
-  {
-    "name": "guardsman_tone1",
-    "unicode": "1F482-1F3FB",
+  "guardsman_tone1": {
+    "category": "people",
+    "moji": "💂🏻",
+    "unicodeVersion": "8.0",
     "digest": "c531ecb101bdf9ce1db18e1567882e6db927410237100b0a2492a1401860246e"
   },
-  {
-    "name": "guardsman_tone2",
-    "unicode": "1F482-1F3FC",
+  "guardsman_tone2": {
+    "category": "people",
+    "moji": "💂🏼",
+    "unicodeVersion": "8.0",
     "digest": "602168c5204af0f1de8b4aa5863b192ef20c19d263999377aa5eb60f98311732"
   },
-  {
-    "name": "guardsman_tone3",
-    "unicode": "1F482-1F3FD",
+  "guardsman_tone3": {
+    "category": "people",
+    "moji": "💂🏽",
+    "unicodeVersion": "8.0",
     "digest": "d0a85de46dd02c7bd6cb14bff0f22d2db9083d4b171a8806c83363b49f3dd9ef"
   },
-  {
-    "name": "guardsman_tone4",
-    "unicode": "1F482-1F3FE",
+  "guardsman_tone4": {
+    "category": "people",
+    "moji": "💂🏾",
+    "unicodeVersion": "8.0",
     "digest": "1c9d4d72b6b50bdac8271613b6d2a38340ec2067bc344e8ee2a3c863fd5c23a1"
   },
-  {
-    "name": "guardsman_tone5",
-    "unicode": "1F482-1F3FF",
+  "guardsman_tone5": {
+    "category": "people",
+    "moji": "💂🏿",
+    "unicodeVersion": "8.0",
     "digest": "9899a796d01842e495d716fbe737a16d85724f7d3e23f50807ec2bc70f057318"
   },
-  {
-    "name": "guitar",
-    "unicode": "1F3B8",
+  "guitar": {
+    "category": "activity",
+    "moji": "🎸",
+    "unicodeVersion": "6.0",
     "digest": "a1027ceae4dd3ea270740587c9d373329e5677e375c9e00af6ae3275e0b67500"
   },
-  {
-    "name": "gun",
-    "unicode": "1F52B",
+  "gun": {
+    "category": "objects",
+    "moji": "🔫",
+    "unicodeVersion": "6.0",
     "digest": "fc12b577df2283e7b336f23774f9cfe5b79f1d26ddd28a64a560519b28d94ca5"
   },
-  {
-    "name": "haircut",
-    "unicode": "1F487",
+  "haircut": {
+    "category": "people",
+    "moji": "💇",
+    "unicodeVersion": "6.0",
     "digest": "b243a04f5ca889accd45e7abe095ac5caa92274ed95103f5966a36b415fff412"
   },
-  {
-    "name": "haircut_tone1",
-    "unicode": "1F487-1F3FB",
+  "haircut_tone1": {
+    "category": "people",
+    "moji": "💇🏻",
+    "unicodeVersion": "8.0",
     "digest": "a58d0cff1427b80dfd7a9ea5267b4a181e9faaac6a51a0165db522f668b4cf91"
   },
-  {
-    "name": "haircut_tone2",
-    "unicode": "1F487-1F3FC",
+  "haircut_tone2": {
+    "category": "people",
+    "moji": "💇🏼",
+    "unicodeVersion": "8.0",
     "digest": "675083ff40001405f8de99268477d50dd8594ff6ca40ddfd442dd42ad76e8216"
   },
-  {
-    "name": "haircut_tone3",
-    "unicode": "1F487-1F3FD",
+  "haircut_tone3": {
+    "category": "people",
+    "moji": "💇🏽",
+    "unicodeVersion": "8.0",
     "digest": "70d7581e49c315a3771dd61a3713229886db32aaaeb3af078a69cc042f809150"
   },
-  {
-    "name": "haircut_tone4",
-    "unicode": "1F487-1F3FE",
+  "haircut_tone4": {
+    "category": "people",
+    "moji": "💇🏾",
+    "unicodeVersion": "8.0",
     "digest": "ec5e3e909eb3bc375ef9cc0fe0e0f90b33f44f273ada91ccf415bbc43b8ffbfc"
   },
-  {
-    "name": "haircut_tone5",
-    "unicode": "1F487-1F3FF",
+  "haircut_tone5": {
+    "category": "people",
+    "moji": "💇🏿",
+    "unicodeVersion": "8.0",
     "digest": "7c89739ee458546a808fded7f96d9354c47a76883ebb262d5f5abeafd021260e"
   },
-  {
-    "name": "hamburger",
-    "unicode": "1F354",
+  "hamburger": {
+    "category": "food",
+    "moji": "🍔",
+    "unicodeVersion": "6.0",
     "digest": "48204235238bd89d3a69f319f65135102f3d6b181eec241d4d86b302bbffa9bf"
   },
-  {
-    "name": "hammer",
-    "unicode": "1F528",
+  "hammer": {
+    "category": "objects",
+    "moji": "🔨",
+    "unicodeVersion": "6.0",
     "digest": "d0e7830539d935fcd82820c4e0c1d724f0756dfc83a51171fe0f4b36b69fac42"
   },
-  {
-    "name": "hammer_pick",
-    "unicode": "2692",
-    "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
-  },
-  {
-    "name": "hammer_and_pick",
-    "unicode": "2692",
+  "hammer_pick": {
+    "category": "objects",
+    "moji": "⚒",
+    "unicodeVersion": "4.1",
     "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
   },
-  {
-    "name": "hamster",
-    "unicode": "1F439",
+  "hamster": {
+    "category": "nature",
+    "moji": "🐹",
+    "unicodeVersion": "6.0",
     "digest": "a7e7582e8b1bccd5b7df27ccb05e353a3f0e39bdeb40877732706b9d74a70de1"
   },
-  {
-    "name": "hand_splayed",
-    "unicode": "1F590",
+  "hand_splayed": {
+    "category": "people",
+    "moji": "🖐",
+    "unicodeVersion": "7.0",
     "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
   },
-  {
-    "name": "raised_hand_with_fingers_splayed",
-    "unicode": "1F590",
-    "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
-  },
-  {
-    "name": "hand_splayed_tone1",
-    "unicode": "1F590-1F3FB",
-    "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
-  },
-  {
-    "name": "raised_hand_with_fingers_splayed_tone1",
-    "unicode": "1F590-1F3FB",
+  "hand_splayed_tone1": {
+    "category": "people",
+    "moji": "🖐🏻",
+    "unicodeVersion": "8.0",
     "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
   },
-  {
-    "name": "hand_splayed_tone2",
-    "unicode": "1F590-1F3FC",
+  "hand_splayed_tone2": {
+    "category": "people",
+    "moji": "🖐🏼",
+    "unicodeVersion": "8.0",
     "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
   },
-  {
-    "name": "raised_hand_with_fingers_splayed_tone2",
-    "unicode": "1F590-1F3FC",
-    "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
-  },
-  {
-    "name": "hand_splayed_tone3",
-    "unicode": "1F590-1F3FD",
-    "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
-  },
-  {
-    "name": "raised_hand_with_fingers_splayed_tone3",
-    "unicode": "1F590-1F3FD",
+  "hand_splayed_tone3": {
+    "category": "people",
+    "moji": "🖐🏽",
+    "unicodeVersion": "8.0",
     "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
   },
-  {
-    "name": "hand_splayed_tone4",
-    "unicode": "1F590-1F3FE",
+  "hand_splayed_tone4": {
+    "category": "people",
+    "moji": "🖐🏾",
+    "unicodeVersion": "8.0",
     "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
   },
-  {
-    "name": "raised_hand_with_fingers_splayed_tone4",
-    "unicode": "1F590-1F3FE",
-    "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
-  },
-  {
-    "name": "hand_splayed_tone5",
-    "unicode": "1F590-1F3FF",
-    "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
-  },
-  {
-    "name": "raised_hand_with_fingers_splayed_tone5",
-    "unicode": "1F590-1F3FF",
+  "hand_splayed_tone5": {
+    "category": "people",
+    "moji": "🖐🏿",
+    "unicodeVersion": "8.0",
     "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
   },
-  {
-    "name": "handbag",
-    "unicode": "1F45C",
+  "handbag": {
+    "category": "people",
+    "moji": "👜",
+    "unicodeVersion": "6.0",
     "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45"
   },
-  {
-    "name": "handball",
-    "unicode": "1F93E",
+  "handball": {
+    "category": "activity",
+    "moji": "🤾",
+    "unicodeVersion": "9.0",
     "digest": "94ceb28024eb3259d8b137cafd7438773e717fbc04f5da810f85e43ca0fa9e00"
   },
-  {
-    "name": "handball_tone1",
-    "unicode": "1F93E-1F3FB",
+  "handball_tone1": {
+    "category": "activity",
+    "moji": "🤾🏻",
+    "unicodeVersion": "9.0",
     "digest": "8bec4de0d05c80e335e44d65598d186ca92696977353c9fd9c2a5efa122cb842"
   },
-  {
-    "name": "handball_tone2",
-    "unicode": "1F93E-1F3FC",
+  "handball_tone2": {
+    "category": "activity",
+    "moji": "🤾🏼",
+    "unicodeVersion": "9.0",
     "digest": "2ff4131e1e2f089b315d8e176c9348877c26c2bd03706fb75d41bc61bc99bf93"
   },
-  {
-    "name": "handball_tone3",
-    "unicode": "1F93E-1F3FD",
+  "handball_tone3": {
+    "category": "activity",
+    "moji": "🤾🏽",
+    "unicodeVersion": "9.0",
     "digest": "224a71f94dd37d3729325d11412334667a81422e21f6d7c008730ff350f51a80"
   },
-  {
-    "name": "handball_tone4",
-    "unicode": "1F93E-1F3FE",
+  "handball_tone4": {
+    "category": "activity",
+    "moji": "🤾🏾",
+    "unicodeVersion": "9.0",
     "digest": "a5f7a9db790565981bad2d0d9e09554c8c509a8179b4705a418300d58a7894b4"
   },
-  {
-    "name": "handball_tone5",
-    "unicode": "1F93E-1F3FF",
+  "handball_tone5": {
+    "category": "activity",
+    "moji": "🤾🏿",
+    "unicodeVersion": "9.0",
     "digest": "00404572d4683f2e8e8a494aa733e96fbec1723634d0a8cb8d75f2829a789d27"
   },
-  {
-    "name": "handshake",
-    "unicode": "1F91D",
+  "handshake": {
+    "category": "people",
+    "moji": "🤝",
+    "unicodeVersion": "9.0",
     "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
   },
-  {
-    "name": "shaking_hands",
-    "unicode": "1F91D",
-    "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
-  },
-  {
-    "name": "handshake_tone1",
-    "unicode": "1F91D-1F3FB",
-    "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
-  },
-  {
-    "name": "shaking_hands_tone1",
-    "unicode": "1F91D-1F3FB",
+  "handshake_tone1": {
+    "category": "people",
+    "moji": "🤝🏻",
+    "unicodeVersion": "9.0",
     "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
   },
-  {
-    "name": "handshake_tone2",
-    "unicode": "1F91D-1F3FC",
+  "handshake_tone2": {
+    "category": "people",
+    "moji": "🤝🏼",
+    "unicodeVersion": "9.0",
     "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
   },
-  {
-    "name": "shaking_hands_tone2",
-    "unicode": "1F91D-1F3FC",
-    "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
-  },
-  {
-    "name": "handshake_tone3",
-    "unicode": "1F91D-1F3FD",
-    "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
-  },
-  {
-    "name": "shaking_hands_tone3",
-    "unicode": "1F91D-1F3FD",
+  "handshake_tone3": {
+    "category": "people",
+    "moji": "🤝🏽",
+    "unicodeVersion": "9.0",
     "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
   },
-  {
-    "name": "handshake_tone4",
-    "unicode": "1F91D-1F3FE",
+  "handshake_tone4": {
+    "category": "people",
+    "moji": "🤝🏾",
+    "unicodeVersion": "9.0",
     "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
   },
-  {
-    "name": "shaking_hands_tone4",
-    "unicode": "1F91D-1F3FE",
-    "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
-  },
-  {
-    "name": "handshake_tone5",
-    "unicode": "1F91D-1F3FF",
-    "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
-  },
-  {
-    "name": "shaking_hands_tone5",
-    "unicode": "1F91D-1F3FF",
+  "handshake_tone5": {
+    "category": "people",
+    "moji": "🤝🏿",
+    "unicodeVersion": "9.0",
     "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
   },
-  {
-    "name": "hash",
-    "unicode": "0023-20E3",
+  "hash": {
+    "category": "symbols",
+    "moji": "#⃣",
+    "unicodeVersion": "3.0",
     "digest": "01c8b577953010bff0c20f797c2c96ab5d98d4e6ac179c4895a78f34ea904655"
   },
-  {
-    "name": "hatched_chick",
-    "unicode": "1F425",
+  "hatched_chick": {
+    "category": "nature",
+    "moji": "🐥",
+    "unicodeVersion": "6.0",
     "digest": "006571b9e9e839ec9fcb1a911b935c8ca71eb8bcdce9775bee6a2a4c7c927277"
   },
-  {
-    "name": "hatching_chick",
-    "unicode": "1F423",
+  "hatching_chick": {
+    "category": "nature",
+    "moji": "🐣",
+    "unicodeVersion": "6.0",
     "digest": "fd7f69fa186407f80de59dec5116e318325a5743ee0e8bba1db541f1e57e7f74"
   },
-  {
-    "name": "head_bandage",
-    "unicode": "1F915",
+  "head_bandage": {
+    "category": "people",
+    "moji": "🤕",
+    "unicodeVersion": "8.0",
     "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
   },
-  {
-    "name": "face_with_head_bandage",
-    "unicode": "1F915",
-    "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
-  },
-  {
-    "name": "headphones",
-    "unicode": "1F3A7",
+  "headphones": {
+    "category": "activity",
+    "moji": "🎧",
+    "unicodeVersion": "6.0",
     "digest": "34f9d5598158d5d6f978a5ea5c5aa9948bb2990625565a3afad7710f864fbe2f"
   },
-  {
-    "name": "hear_no_evil",
-    "unicode": "1F649",
+  "hear_no_evil": {
+    "category": "nature",
+    "moji": "🙉",
+    "unicodeVersion": "6.0",
     "digest": "53b030b6d6f4ed1a734fa7d48b46f42eb1b2b01653202c1838b742082f08c4bf"
   },
-  {
-    "name": "heart",
-    "unicode": "2764",
+  "heart": {
+    "category": "symbols",
+    "moji": "❤",
+    "unicodeVersion": "1.1",
     "digest": "92be652ec3e50c6e7393440b5d52b88a367f98a28dffe12660095ed3253aa6c0"
   },
-  {
-    "name": "heart_decoration",
-    "unicode": "1F49F",
+  "heart_decoration": {
+    "category": "symbols",
+    "moji": "💟",
+    "unicodeVersion": "6.0",
     "digest": "6ec5bbf3aa75c6f43eb3dc05e9204366936e8b6b4219310bacdc2fc45f51e245"
   },
-  {
-    "name": "heart_exclamation",
-    "unicode": "2763",
-    "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
-  },
-  {
-    "name": "heavy_heart_exclamation_mark_ornament",
-    "unicode": "2763",
+  "heart_exclamation": {
+    "category": "symbols",
+    "moji": "❣",
+    "unicodeVersion": "1.1",
     "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
   },
-  {
-    "name": "heart_eyes",
-    "unicode": "1F60D",
+  "heart_eyes": {
+    "category": "people",
+    "moji": "😍",
+    "unicodeVersion": "6.0",
     "digest": "0eff616517a6252ec89d47d9b4ad85589bcf2bdc7f490578934350acb84b2fcc"
   },
-  {
-    "name": "heart_eyes_cat",
-    "unicode": "1F63B",
+  "heart_eyes_cat": {
+    "category": "people",
+    "moji": "😻",
+    "unicodeVersion": "6.0",
     "digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6"
   },
-  {
-    "name": "heartbeat",
-    "unicode": "1F493",
+  "heartbeat": {
+    "category": "symbols",
+    "moji": "💓",
+    "unicodeVersion": "6.0",
     "digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe"
   },
-  {
-    "name": "heartpulse",
-    "unicode": "1F497",
+  "heartpulse": {
+    "category": "symbols",
+    "moji": "💗",
+    "unicodeVersion": "6.0",
     "digest": "281d8aebfea37db5b7fe82d9115be167006881fe29ab64a5b09ac92ac27a2309"
   },
-  {
-    "name": "hearts",
-    "unicode": "2665",
+  "hearts": {
+    "category": "symbols",
+    "moji": "♥",
+    "unicodeVersion": "1.1",
     "digest": "271429d12c40be921897005b7bdd08f9518960af1e1e6f56bb0060f1f183651e"
   },
-  {
-    "name": "heavy_check_mark",
-    "unicode": "2714",
+  "heavy_check_mark": {
+    "category": "symbols",
+    "moji": "✔",
+    "unicodeVersion": "1.1",
     "digest": "e347728e1290eb9e7b0742d628e2fd124fc049e0774f8a6ddf8e5286e7318718"
   },
-  {
-    "name": "heavy_division_sign",
-    "unicode": "2797",
+  "heavy_division_sign": {
+    "category": "symbols",
+    "moji": "➗",
+    "unicodeVersion": "6.0",
     "digest": "c1e8c40f0788f140b1c5fcb81ed9b5ce1bcfa5988bb8140ed2808e9cb7e0d651"
   },
-  {
-    "name": "heavy_dollar_sign",
-    "unicode": "1F4B2",
+  "heavy_dollar_sign": {
+    "category": "symbols",
+    "moji": "💲",
+    "unicodeVersion": "6.0",
     "digest": "7cdeef38348654b93d566e01a48973281cb404a63d0b75b3bad51032887f3f55"
   },
-  {
-    "name": "heavy_minus_sign",
-    "unicode": "2796",
+  "heavy_minus_sign": {
+    "category": "symbols",
+    "moji": "➖",
+    "unicodeVersion": "6.0",
     "digest": "e5335cc6b22abdce49a6127c34269b65a4a6643ddd3253d9baac425089143e7d"
   },
-  {
-    "name": "heavy_multiplication_x",
-    "unicode": "2716",
+  "heavy_multiplication_x": {
+    "category": "symbols",
+    "moji": "✖",
+    "unicodeVersion": "1.1",
     "digest": "64bbe9e9716a922e405d2f6d3b6d803863a53fac80ff8cd775899971046cb1ca"
   },
-  {
-    "name": "heavy_plus_sign",
-    "unicode": "2795",
+  "heavy_plus_sign": {
+    "category": "symbols",
+    "moji": "➕",
+    "unicodeVersion": "6.0",
     "digest": "d0d8ade2020ceb252205180b85c66e665856e6cb505518d395b9913b0b24b746"
   },
-  {
-    "name": "helicopter",
-    "unicode": "1F681",
+  "helicopter": {
+    "category": "travel",
+    "moji": "🚁",
+    "unicodeVersion": "6.0",
     "digest": "4bd6fd13650fbe3a19cfffeffe6c21b1cda74bd6af64c5dc5999185e35444bc3"
   },
-  {
-    "name": "helmet_with_cross",
-    "unicode": "26D1",
+  "helmet_with_cross": {
+    "category": "people",
+    "moji": "⛑",
+    "unicodeVersion": "5.2",
     "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
   },
-  {
-    "name": "helmet_with_white_cross",
-    "unicode": "26D1",
-    "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
-  },
-  {
-    "name": "herb",
-    "unicode": "1F33F",
+  "herb": {
+    "category": "nature",
+    "moji": "🌿",
+    "unicodeVersion": "6.0",
     "digest": "9fe8ed65515ede59d0926dcf98f14e2498785e1965610aa0dd56eca9b4bedad9"
   },
-  {
-    "name": "hibiscus",
-    "unicode": "1F33A",
+  "hibiscus": {
+    "category": "nature",
+    "moji": "🌺",
+    "unicodeVersion": "6.0",
     "digest": "c442e8eacbd8727bd154bd39692a9a2a03ea2f674b9670ad8361f78a038afe49"
   },
-  {
-    "name": "high_brightness",
-    "unicode": "1F506",
+  "high_brightness": {
+    "category": "symbols",
+    "moji": "🔆",
+    "unicodeVersion": "6.0",
     "digest": "35ced42426dcfd5214c2c6c577dce84bb708156433945e6b6adaff7ea530cc57"
   },
-  {
-    "name": "high_heel",
-    "unicode": "1F460",
+  "high_heel": {
+    "category": "people",
+    "moji": "👠",
+    "unicodeVersion": "6.0",
     "digest": "1e7c7aba50eb1d02cf1d9aa372caca741a6005cf47f68dfa75b7310c3cb18f05"
   },
-  {
-    "name": "hockey",
-    "unicode": "1F3D2",
+  "hockey": {
+    "category": "activity",
+    "moji": "🏒",
+    "unicodeVersion": "8.0",
     "digest": "2d00fb17baa617e799db8e9b1771cc365bb4545c7633df0123e66e1a6e2ed25d"
   },
-  {
-    "name": "hole",
-    "unicode": "1F573",
+  "hole": {
+    "category": "objects",
+    "moji": "🕳",
+    "unicodeVersion": "7.0",
     "digest": "8b5539f6f24f09d5d68ffd56be5aa2a8a2f753a8dfbf64892fb02c8f2703e920"
   },
-  {
-    "name": "homes",
-    "unicode": "1F3D8",
-    "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
-  },
-  {
-    "name": "house_buildings",
-    "unicode": "1F3D8",
+  "homes": {
+    "category": "travel",
+    "moji": "🏘",
+    "unicodeVersion": "7.0",
     "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
   },
-  {
-    "name": "honey_pot",
-    "unicode": "1F36F",
+  "honey_pot": {
+    "category": "food",
+    "moji": "🍯",
+    "unicodeVersion": "6.0",
     "digest": "f6eec8c32fbd1b461446dc6c5d5031c43e6ee9685dc9b1ea1b839114e48c4eee"
   },
-  {
-    "name": "horse",
-    "unicode": "1F434",
+  "horse": {
+    "category": "nature",
+    "moji": "🐴",
+    "unicodeVersion": "6.0",
     "digest": "e377649a9549835770a2a721a92570f699255f88efa646029638eb8ec5f10e3d"
   },
-  {
-    "name": "horse_racing",
-    "unicode": "1F3C7",
+  "horse_racing": {
+    "category": "activity",
+    "moji": "🏇",
+    "unicodeVersion": "6.0",
     "digest": "3b98e94e9c028ad85b9a750cc61db5ee3ac23cf5ad9243ea3e996b1f772bad54"
   },
-  {
-    "name": "horse_racing_tone1",
-    "unicode": "1F3C7-1F3FB",
+  "horse_racing_tone1": {
+    "category": "activity",
+    "moji": "🏇🏻",
+    "unicodeVersion": "8.0",
     "digest": "382d8e4502ed34fc1bbf1779ce483bc2e22b83f89c91746c11a5d7aea656d446"
   },
-  {
-    "name": "horse_racing_tone2",
-    "unicode": "1F3C7-1F3FC",
+  "horse_racing_tone2": {
+    "category": "activity",
+    "moji": "🏇🏼",
+    "unicodeVersion": "8.0",
     "digest": "198df9973b492ea63e5cfc210dd9591750ccce04a6380adc1dc5b4cb0462a8cd"
   },
-  {
-    "name": "horse_racing_tone3",
-    "unicode": "1F3C7-1F3FD",
+  "horse_racing_tone3": {
+    "category": "activity",
+    "moji": "🏇🏽",
+    "unicodeVersion": "8.0",
     "digest": "a67f95fc92c366750ebad3c4db92982893d67a5ed78163c8cc809ac40d2ab9a3"
   },
-  {
-    "name": "horse_racing_tone4",
-    "unicode": "1F3C7-1F3FE",
+  "horse_racing_tone4": {
+    "category": "activity",
+    "moji": "🏇🏾",
+    "unicodeVersion": "8.0",
     "digest": "986b1706c4a3395b58a8ae3b7609ffdd4424dfefcbf26c88c8085f4f6379734e"
   },
-  {
-    "name": "horse_racing_tone5",
-    "unicode": "1F3C7-1F3FF",
+  "horse_racing_tone5": {
+    "category": "activity",
+    "moji": "🏇🏿",
+    "unicodeVersion": "8.0",
     "digest": "66656b5e3d0f43f16f983f9db6214b07aac73b143eeff6475782f98aa5b9ba53"
   },
-  {
-    "name": "hospital",
-    "unicode": "1F3E5",
+  "hospital": {
+    "category": "travel",
+    "moji": "🏥",
+    "unicodeVersion": "6.0",
     "digest": "034573e76df444f5b0eb7aff3a4103e4b49a1813869155ab3ae29a6fc0c6c8a2"
   },
-  {
-    "name": "hot_pepper",
-    "unicode": "1F336",
+  "hot_pepper": {
+    "category": "food",
+    "moji": "🌶",
+    "unicodeVersion": "7.0",
     "digest": "0b05777d42698196a10db17d04030175b1dfa772d06288f71d666d5f8d3fddbc"
   },
-  {
-    "name": "hotdog",
-    "unicode": "1F32D",
+  "hotdog": {
+    "category": "food",
+    "moji": "🌭",
+    "unicodeVersion": "8.0",
     "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
   },
-  {
-    "name": "hot_dog",
-    "unicode": "1F32D",
-    "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
-  },
-  {
-    "name": "hotel",
-    "unicode": "1F3E8",
+  "hotel": {
+    "category": "travel",
+    "moji": "🏨",
+    "unicodeVersion": "6.0",
     "digest": "2d78e0ad4cfb0caad778c7de49fefd6e8356afe902a43e3f1c40bceb6b0be422"
   },
-  {
-    "name": "hotsprings",
-    "unicode": "2668",
+  "hotsprings": {
+    "category": "symbols",
+    "moji": "♨",
+    "unicodeVersion": "1.1",
     "digest": "4c10c3a974b44693e8cbe91365c8b8d7f14f62db234cc516b6e54c08a6bacaed"
   },
-  {
-    "name": "hourglass",
-    "unicode": "231B",
+  "hourglass": {
+    "category": "objects",
+    "moji": "⌛",
+    "unicodeVersion": "1.1",
     "digest": "f0bae8392aaf6f75a83f5d8914936b8650665b24ba1b232fa546b71545dd9acd"
   },
-  {
-    "name": "hourglass_flowing_sand",
-    "unicode": "23F3",
+  "hourglass_flowing_sand": {
+    "category": "objects",
+    "moji": "⏳",
+    "unicodeVersion": "6.0",
     "digest": "2d077729f40fc04007a933e97356bd511cbd8be76b8c55962ca3fa0d8b828e23"
   },
-  {
-    "name": "house",
-    "unicode": "1F3E0",
+  "house": {
+    "category": "travel",
+    "moji": "🏠",
+    "unicodeVersion": "6.0",
     "digest": "b4ac25979fbe161ada0d2a75769aa7552d2371d37d78cddba4ffdc7f076d3279"
   },
-  {
-    "name": "house_abandoned",
-    "unicode": "1F3DA",
-    "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
-  },
-  {
-    "name": "derelict_house_building",
-    "unicode": "1F3DA",
+  "house_abandoned": {
+    "category": "travel",
+    "moji": "🏚",
+    "unicodeVersion": "7.0",
     "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
   },
-  {
-    "name": "house_with_garden",
-    "unicode": "1F3E1",
+  "house_with_garden": {
+    "category": "travel",
+    "moji": "🏡",
+    "unicodeVersion": "6.0",
     "digest": "817463f23ec0a849393ba75c333e822b4d253cd4db998c127e90d1b924f35d20"
   },
-  {
-    "name": "hugging",
-    "unicode": "1F917",
+  "hugging": {
+    "category": "people",
+    "moji": "🤗",
+    "unicodeVersion": "8.0",
     "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
   },
-  {
-    "name": "hugging_face",
-    "unicode": "1F917",
-    "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
-  },
-  {
-    "name": "hushed",
-    "unicode": "1F62F",
+  "hushed": {
+    "category": "people",
+    "moji": "😯",
+    "unicodeVersion": "6.1",
     "digest": "22586107f7399eff64538a52929dade152633aa268fc5ec4e6fe1c0e00a7bd89"
   },
-  {
-    "name": "ice_cream",
-    "unicode": "1F368",
+  "ice_cream": {
+    "category": "food",
+    "moji": "🍨",
+    "unicodeVersion": "6.0",
     "digest": "d1a8e685f2ecf83dead28733859e369d6ce120a2669cdab97dc4423547d472ac"
   },
-  {
-    "name": "ice_skate",
-    "unicode": "26F8",
+  "ice_skate": {
+    "category": "activity",
+    "moji": "⛸",
+    "unicodeVersion": "5.2",
     "digest": "41ef65c143bc068868fa64080ffd447d91aa3fe2a39e69ecaa97022820af4dcd"
   },
-  {
-    "name": "icecream",
-    "unicode": "1F366",
+  "icecream": {
+    "category": "food",
+    "moji": "🍦",
+    "unicodeVersion": "6.0",
     "digest": "22cfe17b80cbd2a0377ee90da45bd40d33533c914b2639d363fbb1f00714e194"
   },
-  {
-    "name": "id",
-    "unicode": "1F194",
+  "id": {
+    "category": "symbols",
+    "moji": "🆔",
+    "unicodeVersion": "6.0",
     "digest": "bcf0922e083821d3be7951893084ea0d72a0110ef0b20d11dfec24dd70633893"
   },
-  {
-    "name": "ideograph_advantage",
-    "unicode": "1F250",
+  "ideograph_advantage": {
+    "category": "symbols",
+    "moji": "🉐",
+    "unicodeVersion": "6.0",
     "digest": "0b6bf59f63fda1afa92d652814a778a056c3f4abdd9cf3f6796068bd71783051"
   },
-  {
-    "name": "imp",
-    "unicode": "1F47F",
+  "imp": {
+    "category": "people",
+    "moji": "👿",
+    "unicodeVersion": "6.0",
     "digest": "52598cf2441988f875ccb4e479637baefc679e3ca64e9a6400e56488b0fde811"
   },
-  {
-    "name": "inbox_tray",
-    "unicode": "1F4E5",
+  "inbox_tray": {
+    "category": "objects",
+    "moji": "📥",
+    "unicodeVersion": "6.0",
     "digest": "d5d9497022b5318fcfbfdfcd56df9c65dd8f4a4cb5e6283ca260836df57da301"
   },
-  {
-    "name": "incoming_envelope",
-    "unicode": "1F4E8",
+  "incoming_envelope": {
+    "category": "objects",
+    "moji": "📨",
+    "unicodeVersion": "6.0",
     "digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a"
   },
-  {
-    "name": "information_desk_person",
-    "unicode": "1F481",
+  "information_desk_person": {
+    "category": "people",
+    "moji": "💁",
+    "unicodeVersion": "6.0",
     "digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064"
   },
-  {
-    "name": "information_desk_person_tone1",
-    "unicode": "1F481-1F3FB",
+  "information_desk_person_tone1": {
+    "category": "people",
+    "moji": "💁🏻",
+    "unicodeVersion": "8.0",
     "digest": "6674f2e059eff7cfd7fd6abc800da37c4f1087feb4ff26c9e4e31aa29fdf9921"
   },
-  {
-    "name": "information_desk_person_tone2",
-    "unicode": "1F481-1F3FC",
+  "information_desk_person_tone2": {
+    "category": "people",
+    "moji": "💁🏼",
+    "unicodeVersion": "8.0",
     "digest": "9983412ecd130b7e9cfb078167016c06fd043b6f9f3c26d21733ca3f059fd109"
   },
-  {
-    "name": "information_desk_person_tone3",
-    "unicode": "1F481-1F3FD",
+  "information_desk_person_tone3": {
+    "category": "people",
+    "moji": "💁🏽",
+    "unicodeVersion": "8.0",
     "digest": "d8907bf47af5722127afca8fc0da587eab33044a6c60a94890983deb8d6f7a66"
   },
-  {
-    "name": "information_desk_person_tone4",
-    "unicode": "1F481-1F3FE",
+  "information_desk_person_tone4": {
+    "category": "people",
+    "moji": "💁🏾",
+    "unicodeVersion": "8.0",
     "digest": "3be086d4edfe9ca8e4a364b4e8d09b81b5b594b5eeb9ffdf6370179fb3118658"
   },
-  {
-    "name": "information_desk_person_tone5",
-    "unicode": "1F481-1F3FF",
+  "information_desk_person_tone5": {
+    "category": "people",
+    "moji": "💁🏿",
+    "unicodeVersion": "8.0",
     "digest": "2fde4e98dd11c5c29c89cad7cbb7bd2d5077dfad07913b20e01955b2d0dfad40"
   },
-  {
-    "name": "information_source",
-    "unicode": "2139",
+  "information_source": {
+    "category": "symbols",
+    "moji": "ℹ",
+    "unicodeVersion": "3.0",
     "digest": "b6bf3cce86d42c2e3c46470baab4af01e900b8ae337b605c3da07c3eba671269"
   },
-  {
-    "name": "innocent",
-    "unicode": "1F607",
+  "innocent": {
+    "category": "people",
+    "moji": "😇",
+    "unicodeVersion": "6.0",
     "digest": "20f8d856bc3e46f4b1173cea05d4577e1c61f06b2daba46e57db90f4066bb428"
   },
-  {
-    "name": "interrobang",
-    "unicode": "2049",
+  "interrobang": {
+    "category": "symbols",
+    "moji": "⁉",
+    "unicodeVersion": "3.0",
     "digest": "92a2d5b4c0bd6714e402f6f12fe19774cb41d081b5e9c23c415ce794224d8117"
   },
-  {
-    "name": "iphone",
-    "unicode": "1F4F1",
+  "iphone": {
+    "category": "objects",
+    "moji": "📱",
+    "unicodeVersion": "6.0",
     "digest": "1ebc54215713cd4bf1c1e50770999f2512bb4fea29e37d0bb3a8aa2460ff875d"
   },
-  {
-    "name": "island",
-    "unicode": "1F3DD",
-    "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
-  },
-  {
-    "name": "desert_island",
-    "unicode": "1F3DD",
+  "island": {
+    "category": "travel",
+    "moji": "🏝",
+    "unicodeVersion": "7.0",
     "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
   },
-  {
-    "name": "izakaya_lantern",
-    "unicode": "1F3EE",
+  "izakaya_lantern": {
+    "category": "objects",
+    "moji": "🏮",
+    "unicodeVersion": "6.0",
     "digest": "fbdc290e666d43d0776a73b955c26df4518692b35e72742e073705fc4ca2ae88"
   },
-  {
-    "name": "jack_o_lantern",
-    "unicode": "1F383",
+  "jack_o_lantern": {
+    "category": "nature",
+    "moji": "🎃",
+    "unicodeVersion": "6.0",
     "digest": "78d666c2e80f64bfb6796f53e5ba4960a83ec36192110e8661031bee2b5e370a"
   },
-  {
-    "name": "japan",
-    "unicode": "1F5FE",
+  "japan": {
+    "category": "travel",
+    "moji": "🗾",
+    "unicodeVersion": "6.0",
     "digest": "e7d9d6ebf9047fdd3c52e074ba259659c6d8e51a6abae3cdb8d6cf6dbf9a93fe"
   },
-  {
-    "name": "japanese_castle",
-    "unicode": "1F3EF",
+  "japanese_castle": {
+    "category": "travel",
+    "moji": "🏯",
+    "unicodeVersion": "6.0",
     "digest": "938ae132c403330288223b88d28c19a47224d4f254fbc2366ecef73d9633112c"
   },
-  {
-    "name": "japanese_goblin",
-    "unicode": "1F47A",
+  "japanese_goblin": {
+    "category": "people",
+    "moji": "👺",
+    "unicodeVersion": "6.0",
     "digest": "63d4bcf58b9d0c29612994432aad2ae35819fdd2890674e60a2f1d51601b742e"
   },
-  {
-    "name": "japanese_ogre",
-    "unicode": "1F479",
+  "japanese_ogre": {
+    "category": "people",
+    "moji": "👹",
+    "unicodeVersion": "6.0",
     "digest": "434ceedd102e7dcbc07e086811673dd63659ddf8c3ec4d029a3d759a0abfcbdb"
   },
-  {
-    "name": "jeans",
-    "unicode": "1F456",
+  "jeans": {
+    "category": "people",
+    "moji": "👖",
+    "unicodeVersion": "6.0",
     "digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5"
   },
-  {
-    "name": "joy",
-    "unicode": "1F602",
+  "joy": {
+    "category": "people",
+    "moji": "😂",
+    "unicodeVersion": "6.0",
     "digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08"
   },
-  {
-    "name": "joy_cat",
-    "unicode": "1F639",
+  "joy_cat": {
+    "category": "people",
+    "moji": "😹",
+    "unicodeVersion": "6.0",
     "digest": "a65c999604147e5e20170fcb14f80a1ff0a633f991492e1f790b2ad4caec7b7e"
   },
-  {
-    "name": "joystick",
-    "unicode": "1F579",
+  "joystick": {
+    "category": "objects",
+    "moji": "🕹",
+    "unicodeVersion": "7.0",
     "digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd"
   },
-  {
-    "name": "juggling",
-    "unicode": "1F939",
+  "juggling": {
+    "category": "activity",
+    "moji": "🤹",
+    "unicodeVersion": "9.0",
     "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
   },
-  {
-    "name": "juggler",
-    "unicode": "1F939",
-    "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
-  },
-  {
-    "name": "juggling_tone1",
-    "unicode": "1F939-1F3FB",
-    "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
-  },
-  {
-    "name": "juggler_tone1",
-    "unicode": "1F939-1F3FB",
+  "juggling_tone1": {
+    "category": "activity",
+    "moji": "🤹🏻",
+    "unicodeVersion": "9.0",
     "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
   },
-  {
-    "name": "juggling_tone2",
-    "unicode": "1F939-1F3FC",
+  "juggling_tone2": {
+    "category": "activity",
+    "moji": "🤹🏼",
+    "unicodeVersion": "9.0",
     "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
   },
-  {
-    "name": "juggler_tone2",
-    "unicode": "1F939-1F3FC",
-    "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
-  },
-  {
-    "name": "juggling_tone3",
-    "unicode": "1F939-1F3FD",
-    "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
-  },
-  {
-    "name": "juggler_tone3",
-    "unicode": "1F939-1F3FD",
+  "juggling_tone3": {
+    "category": "activity",
+    "moji": "🤹🏽",
+    "unicodeVersion": "9.0",
     "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
   },
-  {
-    "name": "juggling_tone4",
-    "unicode": "1F939-1F3FE",
+  "juggling_tone4": {
+    "category": "activity",
+    "moji": "🤹🏾",
+    "unicodeVersion": "9.0",
     "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
   },
-  {
-    "name": "juggler_tone4",
-    "unicode": "1F939-1F3FE",
-    "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
-  },
-  {
-    "name": "juggling_tone5",
-    "unicode": "1F939-1F3FF",
-    "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
-  },
-  {
-    "name": "juggler_tone5",
-    "unicode": "1F939-1F3FF",
+  "juggling_tone5": {
+    "category": "activity",
+    "moji": "🤹🏿",
+    "unicodeVersion": "9.0",
     "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
   },
-  {
-    "name": "kaaba",
-    "unicode": "1F54B",
+  "kaaba": {
+    "category": "travel",
+    "moji": "🕋",
+    "unicodeVersion": "8.0",
     "digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6"
   },
-  {
-    "name": "key",
-    "unicode": "1F511",
+  "key": {
+    "category": "objects",
+    "moji": "🔑",
+    "unicodeVersion": "6.0",
     "digest": "66719fa77a50a0827c8d47237e2704c03e38186e6fef80627a765473b2294c2e"
   },
-  {
-    "name": "key2",
-    "unicode": "1F5DD",
+  "key2": {
+    "category": "objects",
+    "moji": "🗝",
+    "unicodeVersion": "7.0",
     "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
   },
-  {
-    "name": "old_key",
-    "unicode": "1F5DD",
-    "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
-  },
-  {
-    "name": "keyboard",
-    "unicode": "2328",
+  "keyboard": {
+    "category": "objects",
+    "moji": "⌨",
+    "unicodeVersion": "1.1",
     "digest": "34da8ff62ca964142f9281b80123dbba74deaac8d77fa61758c30cfb36c31386"
   },
-  {
-    "name": "kimono",
-    "unicode": "1F458",
+  "kimono": {
+    "category": "people",
+    "moji": "👘",
+    "unicodeVersion": "6.0",
     "digest": "637182590e256c8fb74ce4c0565f5180c07f06e3bdebf30138ed3259b209c27f"
   },
-  {
-    "name": "kiss",
-    "unicode": "1F48B",
+  "kiss": {
+    "category": "people",
+    "moji": "💋",
+    "unicodeVersion": "6.0",
     "digest": "62f9b9ffcb01558cd5bb829344a1d1d399511663ff5235405c1f786c9416a94d"
   },
-  {
-    "name": "kiss_mm",
-    "unicode": "1F468-2764-1F48B-1F468",
-    "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
-  },
-  {
-    "name": "couplekiss_mm",
-    "unicode": "1F468-2764-1F48B-1F468",
+  "kiss_mm": {
+    "category": "people",
+    "moji": "👨‍❤️‍💋‍👨",
+    "unicodeVersion": "6.0",
     "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
   },
-  {
-    "name": "kiss_ww",
-    "unicode": "1F469-2764-1F48B-1F469",
+  "kiss_ww": {
+    "category": "people",
+    "moji": "👩‍❤️‍💋‍👩",
+    "unicodeVersion": "6.0",
     "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
   },
-  {
-    "name": "couplekiss_ww",
-    "unicode": "1F469-2764-1F48B-1F469",
-    "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
-  },
-  {
-    "name": "kissing",
-    "unicode": "1F617",
+  "kissing": {
+    "category": "people",
+    "moji": "😗",
+    "unicodeVersion": "6.1",
     "digest": "b4a505f9e3d7fbd0ac60111f0e678cf425a5fd1abc65a3e9db59ae4abcfb8e85"
   },
-  {
-    "name": "kissing_cat",
-    "unicode": "1F63D",
+  "kissing_cat": {
+    "category": "people",
+    "moji": "😽",
+    "unicodeVersion": "6.0",
     "digest": "a00431bf10601db4998e78433279167e52cbd36aed885399482529d5cdab8636"
   },
-  {
-    "name": "kissing_closed_eyes",
-    "unicode": "1F61A",
+  "kissing_closed_eyes": {
+    "category": "people",
+    "moji": "😚",
+    "unicodeVersion": "6.0",
     "digest": "ae474db7daf80fe0b82ae1f2a11672cfcd9f9126e100f6e6d4b8a0d135dce39d"
   },
-  {
-    "name": "kissing_heart",
-    "unicode": "1F618",
+  "kissing_heart": {
+    "category": "people",
+    "moji": "😘",
+    "unicodeVersion": "6.0",
     "digest": "bce372573bd3b347b555c1cd22087e03e650df73c8e0284ab668bf6633251632"
   },
-  {
-    "name": "kissing_smiling_eyes",
-    "unicode": "1F619",
+  "kissing_smiling_eyes": {
+    "category": "people",
+    "moji": "😙",
+    "unicodeVersion": "6.1",
     "digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f"
   },
-  {
-    "name": "kiwi",
-    "unicode": "1F95D",
-    "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
-  },
-  {
-    "name": "kiwifruit",
-    "unicode": "1F95D",
+  "kiwi": {
+    "category": "food",
+    "moji": "🥝",
+    "unicodeVersion": "9.0",
     "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
   },
-  {
-    "name": "knife",
-    "unicode": "1F52A",
+  "knife": {
+    "category": "objects",
+    "moji": "🔪",
+    "unicodeVersion": "6.0",
     "digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df"
   },
-  {
-    "name": "koala",
-    "unicode": "1F428",
+  "koala": {
+    "category": "nature",
+    "moji": "🐨",
+    "unicodeVersion": "6.0",
     "digest": "c58f7e0abae42c2218a85efed0e04151df67187815bebca7f3db6f435e0dab4d"
   },
-  {
-    "name": "koko",
-    "unicode": "1F201",
+  "koko": {
+    "category": "symbols",
+    "moji": "🈁",
+    "unicodeVersion": "6.0",
     "digest": "5f45eb49bbf298e1fadedfe6cccc297850fcaaa4535e4cc911d48d979af55807"
   },
-  {
-    "name": "label",
-    "unicode": "1F3F7",
+  "label": {
+    "category": "objects",
+    "moji": "🏷",
+    "unicodeVersion": "7.0",
     "digest": "9550ed50cedbc56eb1bd22a8a0809d837048a33d6e2e6e7d65c50d95fa05a85d"
   },
-  {
-    "name": "large_blue_circle",
-    "unicode": "1F535",
+  "large_blue_circle": {
+    "category": "symbols",
+    "moji": "🔵",
+    "unicodeVersion": "6.0",
     "digest": "0df3fb3b09a6269459a3d9a1fe78db572190a948680844cfe758f53b6a482ff4"
   },
-  {
-    "name": "large_blue_diamond",
-    "unicode": "1F537",
+  "large_blue_diamond": {
+    "category": "symbols",
+    "moji": "🔷",
+    "unicodeVersion": "6.0",
     "digest": "7f646b4e9de2788ed09e45f72cb512c269dda4989029b39bf9a2556659321651"
   },
-  {
-    "name": "large_orange_diamond",
-    "unicode": "1F536",
+  "large_orange_diamond": {
+    "category": "symbols",
+    "moji": "🔶",
+    "unicodeVersion": "6.0",
     "digest": "80ae005ef9d79190c777f00de0993f8b3cb783f7051d76e971640c8c0827c338"
   },
-  {
-    "name": "last_quarter_moon",
-    "unicode": "1F317",
+  "last_quarter_moon": {
+    "category": "nature",
+    "moji": "🌗",
+    "unicodeVersion": "6.0",
     "digest": "3d1f276607c685d50f4b70d00a57750a57ad9ad84256dafd2dc8eef8c72300c3"
   },
-  {
-    "name": "last_quarter_moon_with_face",
-    "unicode": "1F31C",
+  "last_quarter_moon_with_face": {
+    "category": "nature",
+    "moji": "🌜",
+    "unicodeVersion": "6.0",
     "digest": "d516825ba52dc67f5a01433fb9df2aa77742d38efde4225983ebc4882cbdfe5d"
   },
-  {
-    "name": "laughing",
-    "unicode": "1F606",
+  "laughing": {
+    "category": "people",
+    "moji": "😆",
+    "unicodeVersion": "6.0",
     "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
   },
-  {
-    "name": "satisfied",
-    "unicode": "1F606",
-    "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
-  },
-  {
-    "name": "leaves",
-    "unicode": "1F343",
+  "leaves": {
+    "category": "nature",
+    "moji": "🍃",
+    "unicodeVersion": "6.0",
     "digest": "56a7a0e767a6f214d340d1b5989efd99fec52c6aa306ec5c3328e32234a1631b"
   },
-  {
-    "name": "ledger",
-    "unicode": "1F4D2",
+  "ledger": {
+    "category": "objects",
+    "moji": "📒",
+    "unicodeVersion": "6.0",
     "digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4"
   },
-  {
-    "name": "left_facing_fist",
-    "unicode": "1F91B",
-    "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
-  },
-  {
-    "name": "left_fist",
-    "unicode": "1F91B",
+  "left_facing_fist": {
+    "category": "people",
+    "moji": "🤛",
+    "unicodeVersion": "9.0",
     "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
   },
-  {
-    "name": "left_facing_fist_tone1",
-    "unicode": "1F91B-1F3FB",
+  "left_facing_fist_tone1": {
+    "category": "people",
+    "moji": "🤛🏻",
+    "unicodeVersion": "9.0",
     "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
   },
-  {
-    "name": "left_fist_tone1",
-    "unicode": "1F91B-1F3FB",
-    "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
-  },
-  {
-    "name": "left_facing_fist_tone2",
-    "unicode": "1F91B-1F3FC",
-    "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
-  },
-  {
-    "name": "left_fist_tone2",
-    "unicode": "1F91B-1F3FC",
+  "left_facing_fist_tone2": {
+    "category": "people",
+    "moji": "🤛🏼",
+    "unicodeVersion": "9.0",
     "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
   },
-  {
-    "name": "left_facing_fist_tone3",
-    "unicode": "1F91B-1F3FD",
+  "left_facing_fist_tone3": {
+    "category": "people",
+    "moji": "🤛🏽",
+    "unicodeVersion": "9.0",
     "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
   },
-  {
-    "name": "left_fist_tone3",
-    "unicode": "1F91B-1F3FD",
-    "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
-  },
-  {
-    "name": "left_facing_fist_tone4",
-    "unicode": "1F91B-1F3FE",
-    "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
-  },
-  {
-    "name": "left_fist_tone4",
-    "unicode": "1F91B-1F3FE",
+  "left_facing_fist_tone4": {
+    "category": "people",
+    "moji": "🤛🏾",
+    "unicodeVersion": "9.0",
     "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
   },
-  {
-    "name": "left_facing_fist_tone5",
-    "unicode": "1F91B-1F3FF",
+  "left_facing_fist_tone5": {
+    "category": "people",
+    "moji": "🤛🏿",
+    "unicodeVersion": "9.0",
     "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
   },
-  {
-    "name": "left_fist_tone5",
-    "unicode": "1F91B-1F3FF",
-    "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
-  },
-  {
-    "name": "left_luggage",
-    "unicode": "1F6C5",
+  "left_luggage": {
+    "category": "symbols",
+    "moji": "🛅",
+    "unicodeVersion": "6.0",
     "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf"
   },
-  {
-    "name": "left_right_arrow",
-    "unicode": "2194",
+  "left_right_arrow": {
+    "category": "symbols",
+    "moji": "↔",
+    "unicodeVersion": "1.1",
     "digest": "560fcf1b794eb0d5269c73b3f8da57540cbb8a6f1a9af7a9d10b202252247e34"
   },
-  {
-    "name": "leftwards_arrow_with_hook",
-    "unicode": "21A9",
+  "leftwards_arrow_with_hook": {
+    "category": "symbols",
+    "moji": "↩",
+    "unicodeVersion": "1.1",
     "digest": "504714c5559b1bd35aa469be83069a923d1a25f364cac08c10df0195749e7b26"
   },
-  {
-    "name": "lemon",
-    "unicode": "1F34B",
+  "lemon": {
+    "category": "food",
+    "moji": "🍋",
+    "unicodeVersion": "6.0",
     "digest": "ccca25bb6ac47770dba3aaf75144128f9a73299061969b25a35ad1733dcde5fe"
   },
-  {
-    "name": "leo",
-    "unicode": "264C",
+  "leo": {
+    "category": "symbols",
+    "moji": "♌",
+    "unicodeVersion": "1.1",
     "digest": "f2ed930e279699962f189e0cac519cc29d339b3e82debfdc90c5b0935a7543bb"
   },
-  {
-    "name": "leopard",
-    "unicode": "1F406",
+  "leopard": {
+    "category": "nature",
+    "moji": "🐆",
+    "unicodeVersion": "6.0",
     "digest": "d4a8964b6f2cdf6ddf074d0f1f2f65783a1a43eb4af426905fad0e60899939c7"
   },
-  {
-    "name": "level_slider",
-    "unicode": "1F39A",
+  "level_slider": {
+    "category": "objects",
+    "moji": "🎚",
+    "unicodeVersion": "7.0",
     "digest": "48842324f54d971ebf548a89a82ac7f29e235702081c91b477b1a92d427290e7"
   },
-  {
-    "name": "levitate",
-    "unicode": "1F574",
-    "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
-  },
-  {
-    "name": "man_in_business_suit_levitating",
-    "unicode": "1F574",
+  "levitate": {
+    "category": "activity",
+    "moji": "🕴",
+    "unicodeVersion": "7.0",
     "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
   },
-  {
-    "name": "libra",
-    "unicode": "264E",
+  "libra": {
+    "category": "symbols",
+    "moji": "♎",
+    "unicodeVersion": "1.1",
     "digest": "e330ba05bb449db074bc23d1514246ca5e249110f44ddb5804e5510eef6deac1"
   },
-  {
-    "name": "lifter",
-    "unicode": "1F3CB",
+  "lifter": {
+    "category": "activity",
+    "moji": "🏋",
+    "unicodeVersion": "7.0",
     "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
   },
-  {
-    "name": "weight_lifter",
-    "unicode": "1F3CB",
-    "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
-  },
-  {
-    "name": "lifter_tone1",
-    "unicode": "1F3CB-1F3FB",
-    "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
-  },
-  {
-    "name": "weight_lifter_tone1",
-    "unicode": "1F3CB-1F3FB",
+  "lifter_tone1": {
+    "category": "activity",
+    "moji": "🏋🏻",
+    "unicodeVersion": "8.0",
     "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
   },
-  {
-    "name": "lifter_tone2",
-    "unicode": "1F3CB-1F3FC",
+  "lifter_tone2": {
+    "category": "activity",
+    "moji": "🏋🏼",
+    "unicodeVersion": "8.0",
     "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
   },
-  {
-    "name": "weight_lifter_tone2",
-    "unicode": "1F3CB-1F3FC",
-    "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
-  },
-  {
-    "name": "lifter_tone3",
-    "unicode": "1F3CB-1F3FD",
-    "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
-  },
-  {
-    "name": "weight_lifter_tone3",
-    "unicode": "1F3CB-1F3FD",
+  "lifter_tone3": {
+    "category": "activity",
+    "moji": "🏋🏽",
+    "unicodeVersion": "8.0",
     "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
   },
-  {
-    "name": "lifter_tone4",
-    "unicode": "1F3CB-1F3FE",
+  "lifter_tone4": {
+    "category": "activity",
+    "moji": "🏋🏾",
+    "unicodeVersion": "8.0",
     "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
   },
-  {
-    "name": "weight_lifter_tone4",
-    "unicode": "1F3CB-1F3FE",
-    "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
-  },
-  {
-    "name": "lifter_tone5",
-    "unicode": "1F3CB-1F3FF",
-    "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
-  },
-  {
-    "name": "weight_lifter_tone5",
-    "unicode": "1F3CB-1F3FF",
+  "lifter_tone5": {
+    "category": "activity",
+    "moji": "🏋🏿",
+    "unicodeVersion": "8.0",
     "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
   },
-  {
-    "name": "light_rail",
-    "unicode": "1F688",
+  "light_rail": {
+    "category": "travel",
+    "moji": "🚈",
+    "unicodeVersion": "6.0",
     "digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1"
   },
-  {
-    "name": "link",
-    "unicode": "1F517",
+  "link": {
+    "category": "objects",
+    "moji": "🔗",
+    "unicodeVersion": "6.0",
     "digest": "7bf567aabd1fc38b3d70422f9db3a13b50950cf6207e70962c9938827c196ccb"
   },
-  {
-    "name": "lion_face",
-    "unicode": "1F981",
+  "lion_face": {
+    "category": "nature",
+    "moji": "🦁",
+    "unicodeVersion": "8.0",
     "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
   },
-  {
-    "name": "lion",
-    "unicode": "1F981",
-    "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
-  },
-  {
-    "name": "lips",
-    "unicode": "1F444",
+  "lips": {
+    "category": "people",
+    "moji": "👄",
+    "unicodeVersion": "6.0",
     "digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e"
   },
-  {
-    "name": "lipstick",
-    "unicode": "1F484",
+  "lipstick": {
+    "category": "people",
+    "moji": "💄",
+    "unicodeVersion": "6.0",
     "digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1"
   },
-  {
-    "name": "lizard",
-    "unicode": "1F98E",
+  "lizard": {
+    "category": "nature",
+    "moji": "🦎",
+    "unicodeVersion": "9.0",
     "digest": "fb9191f9eab58b8403d4c4626ccbb14ba05c1f6944011751a8edcc4dd03c66e6"
   },
-  {
-    "name": "lock",
-    "unicode": "1F512",
+  "lock": {
+    "category": "objects",
+    "moji": "🔒",
+    "unicodeVersion": "6.0",
     "digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83"
   },
-  {
-    "name": "lock_with_ink_pen",
-    "unicode": "1F50F",
+  "lock_with_ink_pen": {
+    "category": "objects",
+    "moji": "🔏",
+    "unicodeVersion": "6.0",
     "digest": "7b5e959b26cf7296c7b230fc2be9feb9e38391c5001951a019d16b169a71aba9"
   },
-  {
-    "name": "lollipop",
-    "unicode": "1F36D",
+  "lollipop": {
+    "category": "food",
+    "moji": "🍭",
+    "unicodeVersion": "6.0",
     "digest": "17b6a0df47ec758a2f9c087b46a6902cee344d39407ef4c321e408505cbb72ca"
   },
-  {
-    "name": "loop",
-    "unicode": "27BF",
+  "loop": {
+    "category": "symbols",
+    "moji": "➿",
+    "unicodeVersion": "6.0",
     "digest": "9f20ecc34b3c871789ba7d0712aa31e7a74b6c1558ac8bea385bc40590056726"
   },
-  {
-    "name": "loud_sound",
-    "unicode": "1F50A",
+  "loud_sound": {
+    "category": "symbols",
+    "moji": "🔊",
+    "unicodeVersion": "6.0",
     "digest": "64b12db9ddd8adf74a9fc2bd83c7979ea865113347f7ce8666e9ccf5019e715f"
   },
-  {
-    "name": "loudspeaker",
-    "unicode": "1F4E2",
+  "loudspeaker": {
+    "category": "symbols",
+    "moji": "📢",
+    "unicodeVersion": "6.0",
     "digest": "1e1f35d16dd2898ebaa6f2b2868203df6e09c8a70df069c92d6d1b5cb2ac0976"
   },
-  {
-    "name": "love_hotel",
-    "unicode": "1F3E9",
+  "love_hotel": {
+    "category": "travel",
+    "moji": "🏩",
+    "unicodeVersion": "6.0",
     "digest": "ff8966a50fd47a216855488eb09a367d231fea21f49e7e5325191d32fb494473"
   },
-  {
-    "name": "love_letter",
-    "unicode": "1F48C",
+  "love_letter": {
+    "category": "objects",
+    "moji": "💌",
+    "unicodeVersion": "6.0",
     "digest": "037261c8ca4d72f7205e51664591696da2ae7ceb19f1c1c9f6123da5a5979d29"
   },
-  {
-    "name": "low_brightness",
-    "unicode": "1F505",
+  "low_brightness": {
+    "category": "symbols",
+    "moji": "🔅",
+    "unicodeVersion": "6.0",
     "digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4"
   },
-  {
-    "name": "lying_face",
-    "unicode": "1F925",
-    "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
-  },
-  {
-    "name": "liar",
-    "unicode": "1F925",
+  "lying_face": {
+    "category": "people",
+    "moji": "🤥",
+    "unicodeVersion": "9.0",
     "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
   },
-  {
-    "name": "m",
-    "unicode": "24C2",
+  "m": {
+    "category": "symbols",
+    "moji": "Ⓜ",
+    "unicodeVersion": "1.1",
     "digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4"
   },
-  {
-    "name": "mag",
-    "unicode": "1F50D",
+  "mag": {
+    "category": "objects",
+    "moji": "🔍",
+    "unicodeVersion": "6.0",
     "digest": "a6e31a2efa7d9427aaa30b45d9f4181ee55c44be08aea2df165a86e0e6d9eaa1"
   },
-  {
-    "name": "mag_right",
-    "unicode": "1F50E",
+  "mag_right": {
+    "category": "objects",
+    "moji": "🔎",
+    "unicodeVersion": "6.0",
     "digest": "c7d8ceeb05db261e5eaab31dc4da432d0d5592a2ed71e526c5a542daa230bbaf"
   },
-  {
-    "name": "mahjong",
-    "unicode": "1F004",
+  "mahjong": {
+    "category": "symbols",
+    "moji": "🀄",
+    "unicodeVersion": "5.1",
     "digest": "755d69f988434ce1c17531a8b7ac92ead6f5607c2635a22f10e0ad70f09fc3e6"
   },
-  {
-    "name": "mailbox",
-    "unicode": "1F4EB",
+  "mailbox": {
+    "category": "objects",
+    "moji": "📫",
+    "unicodeVersion": "6.0",
     "digest": "2069091be90a530a43ef29d5ec7688c351bf4d5b08d63a0d20d72b67d639ec62"
   },
-  {
-    "name": "mailbox_closed",
-    "unicode": "1F4EA",
+  "mailbox_closed": {
+    "category": "objects",
+    "moji": "📪",
+    "unicodeVersion": "6.0",
     "digest": "d88d65bfebb8216535fd055c69f319564b2cf0b0901820f8312f581864557ed4"
   },
-  {
-    "name": "mailbox_with_mail",
-    "unicode": "1F4EC",
+  "mailbox_with_mail": {
+    "category": "objects",
+    "moji": "📬",
+    "unicodeVersion": "6.0",
     "digest": "69e966b4659128991a70c6a2dd4d647551bedb91bdf5ce688958686bbec56381"
   },
-  {
-    "name": "mailbox_with_no_mail",
-    "unicode": "1F4ED",
+  "mailbox_with_no_mail": {
+    "category": "objects",
+    "moji": "📭",
+    "unicodeVersion": "6.0",
     "digest": "9e92d8ee88f660ce56da61077c80ec26c5d8f54ebd2306c4cfa16f6c1b981f83"
   },
-  {
-    "name": "man",
-    "unicode": "1F468",
+  "man": {
+    "category": "people",
+    "moji": "👨",
+    "unicodeVersion": "6.0",
     "digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7"
   },
-  {
-    "name": "man_dancing",
-    "unicode": "1F57A",
+  "man_dancing": {
+    "category": "people",
+    "moji": "🕺",
+    "unicodeVersion": "9.0",
     "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
   },
-  {
-    "name": "male_dancer",
-    "unicode": "1F57A",
-    "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
-  },
-  {
-    "name": "man_dancing_tone1",
-    "unicode": "1F57A-1F3FB",
-    "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
-  },
-  {
-    "name": "male_dancer_tone1",
-    "unicode": "1F57A-1F3FB",
+  "man_dancing_tone1": {
+    "category": "activity",
+    "moji": "🕺🏻",
+    "unicodeVersion": "9.0",
     "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
   },
-  {
-    "name": "man_dancing_tone2",
-    "unicode": "1F57A-1F3FC",
+  "man_dancing_tone2": {
+    "category": "activity",
+    "moji": "🕺🏼",
+    "unicodeVersion": "9.0",
     "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
   },
-  {
-    "name": "male_dancer_tone2",
-    "unicode": "1F57A-1F3FC",
-    "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
-  },
-  {
-    "name": "man_dancing_tone3",
-    "unicode": "1F57A-1F3FD",
-    "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
-  },
-  {
-    "name": "male_dancer_tone3",
-    "unicode": "1F57A-1F3FD",
+  "man_dancing_tone3": {
+    "category": "activity",
+    "moji": "🕺🏽",
+    "unicodeVersion": "9.0",
     "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
   },
-  {
-    "name": "man_dancing_tone4",
-    "unicode": "1F57A-1F3FE",
+  "man_dancing_tone4": {
+    "category": "activity",
+    "moji": "🕺🏾",
+    "unicodeVersion": "9.0",
     "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
   },
-  {
-    "name": "male_dancer_tone4",
-    "unicode": "1F57A-1F3FE",
-    "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
-  },
-  {
-    "name": "man_dancing_tone5",
-    "unicode": "1F57A-1F3FF",
-    "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
-  },
-  {
-    "name": "male_dancer_tone5",
-    "unicode": "1F57A-1F3FF",
+  "man_dancing_tone5": {
+    "category": "activity",
+    "moji": "🕺🏿",
+    "unicodeVersion": "9.0",
     "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
   },
-  {
-    "name": "man_in_tuxedo",
-    "unicode": "1F935",
+  "man_in_tuxedo": {
+    "category": "people",
+    "moji": "🤵",
+    "unicodeVersion": "9.0",
     "digest": "4d451a971dfefedc4830ba78e19b123f250e09ae65baddccdc56c0f8aa3a9b50"
   },
-  {
-    "name": "man_in_tuxedo_tone1",
-    "unicode": "1F935-1F3FB",
+  "man_in_tuxedo_tone1": {
+    "category": "people",
+    "moji": "🤵🏻",
+    "unicodeVersion": "9.0",
     "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
   },
-  {
-    "name": "tuxedo_tone1",
-    "unicode": "1F935-1F3FB",
-    "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
-  },
-  {
-    "name": "man_in_tuxedo_tone2",
-    "unicode": "1F935-1F3FC",
-    "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
-  },
-  {
-    "name": "tuxedo_tone2",
-    "unicode": "1F935-1F3FC",
+  "man_in_tuxedo_tone2": {
+    "category": "people",
+    "moji": "🤵🏼",
+    "unicodeVersion": "9.0",
     "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
   },
-  {
-    "name": "man_in_tuxedo_tone3",
-    "unicode": "1F935-1F3FD",
+  "man_in_tuxedo_tone3": {
+    "category": "people",
+    "moji": "🤵🏽",
+    "unicodeVersion": "9.0",
     "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
   },
-  {
-    "name": "tuxedo_tone3",
-    "unicode": "1F935-1F3FD",
-    "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
-  },
-  {
-    "name": "man_in_tuxedo_tone4",
-    "unicode": "1F935-1F3FE",
-    "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
-  },
-  {
-    "name": "tuxedo_tone4",
-    "unicode": "1F935-1F3FE",
+  "man_in_tuxedo_tone4": {
+    "category": "people",
+    "moji": "🤵🏾",
+    "unicodeVersion": "9.0",
     "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
   },
-  {
-    "name": "man_in_tuxedo_tone5",
-    "unicode": "1F935-1F3FF",
+  "man_in_tuxedo_tone5": {
+    "category": "people",
+    "moji": "🤵🏿",
+    "unicodeVersion": "9.0",
     "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
   },
-  {
-    "name": "tuxedo_tone5",
-    "unicode": "1F935-1F3FF",
-    "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
-  },
-  {
-    "name": "man_tone1",
-    "unicode": "1F468-1F3FB",
+  "man_tone1": {
+    "category": "people",
+    "moji": "👨🏻",
+    "unicodeVersion": "8.0",
     "digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504"
   },
-  {
-    "name": "man_tone2",
-    "unicode": "1F468-1F3FC",
+  "man_tone2": {
+    "category": "people",
+    "moji": "👨🏼",
+    "unicodeVersion": "8.0",
     "digest": "7ebc64de40d3ac60fb761be5cf94f53fa10b4f03fb66add46c90f5d98eaf71eb"
   },
-  {
-    "name": "man_tone3",
-    "unicode": "1F468-1F3FD",
+  "man_tone3": {
+    "category": "people",
+    "moji": "👨🏽",
+    "unicodeVersion": "8.0",
     "digest": "77ceef4d3740ed4751acb83dd45b6b754cf625c522c6757309cd4d61202d7149"
   },
-  {
-    "name": "man_tone4",
-    "unicode": "1F468-1F3FE",
+  "man_tone4": {
+    "category": "people",
+    "moji": "👨🏾",
+    "unicodeVersion": "8.0",
     "digest": "41e6037c393f61cca61b9a81b27ed14a95d75fe380e3a00153c33a371a836ffd"
   },
-  {
-    "name": "man_tone5",
-    "unicode": "1F468-1F3FF",
+  "man_tone5": {
+    "category": "people",
+    "moji": "👨🏿",
+    "unicodeVersion": "8.0",
     "digest": "a8cebfd39a5b9c79af7cc37f205e1135376056fee287af967c9f55d415572d99"
   },
-  {
-    "name": "man_with_gua_pi_mao",
-    "unicode": "1F472",
+  "man_with_gua_pi_mao": {
+    "category": "people",
+    "moji": "👲",
+    "unicodeVersion": "6.0",
     "digest": "3dae285e900c69986a48db0fa89d4f371a49f38608059cdae52be098030c5ac4"
   },
-  {
-    "name": "man_with_gua_pi_mao_tone1",
-    "unicode": "1F472-1F3FB",
+  "man_with_gua_pi_mao_tone1": {
+    "category": "people",
+    "moji": "👲🏻",
+    "unicodeVersion": "8.0",
     "digest": "35404d8e266920c78edd9e7143fb052b42f65242a5698494c4f4365e9183cc67"
   },
-  {
-    "name": "man_with_gua_pi_mao_tone2",
-    "unicode": "1F472-1F3FC",
+  "man_with_gua_pi_mao_tone2": {
+    "category": "people",
+    "moji": "👲🏼",
+    "unicodeVersion": "8.0",
     "digest": "82d4f968665a93c7543372c8a1eeb0f25d0ea6842d5e518bd91c226c6c3ab8c2"
   },
-  {
-    "name": "man_with_gua_pi_mao_tone3",
-    "unicode": "1F472-1F3FD",
+  "man_with_gua_pi_mao_tone3": {
+    "category": "people",
+    "moji": "👲🏽",
+    "unicodeVersion": "8.0",
     "digest": "f44159f0c672b9b833449382896180e799abf574f5b3c6cd9541caa992fa18ce"
   },
-  {
-    "name": "man_with_gua_pi_mao_tone4",
-    "unicode": "1F472-1F3FE",
+  "man_with_gua_pi_mao_tone4": {
+    "category": "people",
+    "moji": "👲🏾",
+    "unicodeVersion": "8.0",
     "digest": "c79060188f9461ca34eaa225b7682d8c410883609509fb731c992db69bfeeb50"
   },
-  {
-    "name": "man_with_gua_pi_mao_tone5",
-    "unicode": "1F472-1F3FF",
+  "man_with_gua_pi_mao_tone5": {
+    "category": "people",
+    "moji": "👲🏿",
+    "unicodeVersion": "8.0",
     "digest": "de9e4acdb10f7abddeeabc0b48d91139fc8b544a601c530db811f099991b0d38"
   },
-  {
-    "name": "man_with_turban",
-    "unicode": "1F473",
+  "man_with_turban": {
+    "category": "people",
+    "moji": "👳",
+    "unicodeVersion": "6.0",
     "digest": "db72c944e93983f38d00e3e936ebb5b243c6069f1f1236d46f6a9f1beb8d6634"
   },
-  {
-    "name": "man_with_turban_tone1",
-    "unicode": "1F473-1F3FB",
+  "man_with_turban_tone1": {
+    "category": "people",
+    "moji": "👳🏻",
+    "unicodeVersion": "8.0",
     "digest": "b6d7489c4cd151af09fff48b62c54c336303e14866e6ef38f94cd834b085d09e"
   },
-  {
-    "name": "man_with_turban_tone2",
-    "unicode": "1F473-1F3FC",
+  "man_with_turban_tone2": {
+    "category": "people",
+    "moji": "👳🏼",
+    "unicodeVersion": "8.0",
     "digest": "7854ef973c21847f452d7e78e5c460ea300e12b539ce92c69dabe8f1bf3a4382"
   },
-  {
-    "name": "man_with_turban_tone3",
-    "unicode": "1F473-1F3FD",
+  "man_with_turban_tone3": {
+    "category": "people",
+    "moji": "👳🏽",
+    "unicodeVersion": "8.0",
     "digest": "1dbd9bd78f5263cbadee7d0d5754c14cfbc914f7329e25fbd97d9f5b8ce0737e"
   },
-  {
-    "name": "man_with_turban_tone4",
-    "unicode": "1F473-1F3FE",
+  "man_with_turban_tone4": {
+    "category": "people",
+    "moji": "👳🏾",
+    "unicodeVersion": "8.0",
     "digest": "4f4804da4a7c98ad4f9db3ae3eaf674c8977c638e73414e33ef1f65098e413a3"
   },
-  {
-    "name": "man_with_turban_tone5",
-    "unicode": "1F473-1F3FF",
+  "man_with_turban_tone5": {
+    "category": "people",
+    "moji": "👳🏿",
+    "unicodeVersion": "8.0",
     "digest": "240282aa346ef9b1d0d475ea93a02597697f0f56f086305879b532b0b933210a"
   },
-  {
-    "name": "mans_shoe",
-    "unicode": "1F45E",
+  "mans_shoe": {
+    "category": "people",
+    "moji": "👞",
+    "unicodeVersion": "6.0",
     "digest": "f53fe74abd9906cd3e2dd7e7bddbe1feb9f8f7be28b807fabe452f1f60ca1b84"
   },
-  {
-    "name": "map",
-    "unicode": "1F5FA",
-    "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
-  },
-  {
-    "name": "world_map",
-    "unicode": "1F5FA",
+  "map": {
+    "category": "objects",
+    "moji": "🗺",
+    "unicodeVersion": "7.0",
     "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
   },
-  {
-    "name": "maple_leaf",
-    "unicode": "1F341",
+  "maple_leaf": {
+    "category": "nature",
+    "moji": "🍁",
+    "unicodeVersion": "6.0",
     "digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72"
   },
-  {
-    "name": "martial_arts_uniform",
-    "unicode": "1F94B",
+  "martial_arts_uniform": {
+    "category": "activity",
+    "moji": "🥋",
+    "unicodeVersion": "9.0",
     "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
   },
-  {
-    "name": "karate_uniform",
-    "unicode": "1F94B",
-    "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
-  },
-  {
-    "name": "mask",
-    "unicode": "1F637",
+  "mask": {
+    "category": "people",
+    "moji": "😷",
+    "unicodeVersion": "6.0",
     "digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600"
   },
-  {
-    "name": "massage",
-    "unicode": "1F486",
+  "massage": {
+    "category": "people",
+    "moji": "💆",
+    "unicodeVersion": "6.0",
     "digest": "6ee48b4d8cec0bf31e11d7803ad9fc1f909457c8c00cb320b5671395af3c170c"
   },
-  {
-    "name": "massage_tone1",
-    "unicode": "1F486-1F3FB",
+  "massage_tone1": {
+    "category": "people",
+    "moji": "💆🏻",
+    "unicodeVersion": "8.0",
     "digest": "9da162c2f39628156b87db986a6ada59372a9e9a6b3f0488d21c9e65ec3309bb"
   },
-  {
-    "name": "massage_tone2",
-    "unicode": "1F486-1F3FC",
+  "massage_tone2": {
+    "category": "people",
+    "moji": "💆🏼",
+    "unicodeVersion": "8.0",
     "digest": "ac259188549b5b429b8c4929e1da2314859e8857ee49720551467aedfcc96567"
   },
-  {
-    "name": "massage_tone3",
-    "unicode": "1F486-1F3FD",
+  "massage_tone3": {
+    "category": "people",
+    "moji": "💆🏽",
+    "unicodeVersion": "8.0",
     "digest": "cfd9c105b6debc10448f172afcb20d4192899f7ae5aa8af54c834153a5466364"
   },
-  {
-    "name": "massage_tone4",
-    "unicode": "1F486-1F3FE",
+  "massage_tone4": {
+    "category": "people",
+    "moji": "💆🏾",
+    "unicodeVersion": "8.0",
     "digest": "38ab715c621c58454f3cb09153a96380118cf082568554b6edc5f83fb62e9297"
   },
-  {
-    "name": "massage_tone5",
-    "unicode": "1F486-1F3FF",
+  "massage_tone5": {
+    "category": "people",
+    "moji": "💆🏿",
+    "unicodeVersion": "8.0",
     "digest": "32480457734121b0c83e9be6d693ae379c95535f43f963c0c2f0f20434ee12c6"
   },
-  {
-    "name": "meat_on_bone",
-    "unicode": "1F356",
+  "meat_on_bone": {
+    "category": "food",
+    "moji": "🍖",
+    "unicodeVersion": "6.0",
     "digest": "d71a8e0b118d5e6ca60690793ce9649afb78e707fcbd7be890a75564c94434fd"
   },
-  {
-    "name": "medal",
-    "unicode": "1F3C5",
-    "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
-  },
-  {
-    "name": "sports_medal",
-    "unicode": "1F3C5",
+  "medal": {
+    "category": "activity",
+    "moji": "🏅",
+    "unicodeVersion": "7.0",
     "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
   },
-  {
-    "name": "mega",
-    "unicode": "1F4E3",
+  "mega": {
+    "category": "symbols",
+    "moji": "📣",
+    "unicodeVersion": "6.0",
     "digest": "4b1def6b5b051c5045514063f0ac006222ad81fbfe56d840e14bb950713e331b"
   },
-  {
-    "name": "melon",
-    "unicode": "1F348",
+  "melon": {
+    "category": "food",
+    "moji": "🍈",
+    "unicodeVersion": "6.0",
     "digest": "0cdd663e6f2129808856cdf0746e6571b62aac641f224adb553baf3bb63ba3bd"
   },
-  {
-    "name": "menorah",
-    "unicode": "1F54E",
+  "menorah": {
+    "category": "symbols",
+    "moji": "🕎",
+    "unicodeVersion": "8.0",
     "digest": "49fca8c3bc00ea69653ee2f8d4e21e561856ba39716c13e9d107db3e805a2997"
   },
-  {
-    "name": "mens",
-    "unicode": "1F6B9",
+  "mens": {
+    "category": "symbols",
+    "moji": "🚹",
+    "unicodeVersion": "6.0",
     "digest": "7d92292586ee12a5d1a557c37da4d14708dc3ce701cf32d3280dcc83d91e5df8"
   },
-  {
-    "name": "metal",
-    "unicode": "1F918",
+  "metal": {
+    "category": "people",
+    "moji": "🤘",
+    "unicodeVersion": "8.0",
     "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
   },
-  {
-    "name": "sign_of_the_horns",
-    "unicode": "1F918",
-    "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
-  },
-  {
-    "name": "metal_tone1",
-    "unicode": "1F918-1F3FB",
-    "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
-  },
-  {
-    "name": "sign_of_the_horns_tone1",
-    "unicode": "1F918-1F3FB",
+  "metal_tone1": {
+    "category": "people",
+    "moji": "🤘🏻",
+    "unicodeVersion": "8.0",
     "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
   },
-  {
-    "name": "metal_tone2",
-    "unicode": "1F918-1F3FC",
+  "metal_tone2": {
+    "category": "people",
+    "moji": "🤘🏼",
+    "unicodeVersion": "8.0",
     "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
   },
-  {
-    "name": "sign_of_the_horns_tone2",
-    "unicode": "1F918-1F3FC",
-    "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
-  },
-  {
-    "name": "metal_tone3",
-    "unicode": "1F918-1F3FD",
-    "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
-  },
-  {
-    "name": "sign_of_the_horns_tone3",
-    "unicode": "1F918-1F3FD",
+  "metal_tone3": {
+    "category": "people",
+    "moji": "🤘🏽",
+    "unicodeVersion": "8.0",
     "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
   },
-  {
-    "name": "metal_tone4",
-    "unicode": "1F918-1F3FE",
+  "metal_tone4": {
+    "category": "people",
+    "moji": "🤘🏾",
+    "unicodeVersion": "8.0",
     "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
   },
-  {
-    "name": "sign_of_the_horns_tone4",
-    "unicode": "1F918-1F3FE",
-    "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
-  },
-  {
-    "name": "metal_tone5",
-    "unicode": "1F918-1F3FF",
-    "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
-  },
-  {
-    "name": "sign_of_the_horns_tone5",
-    "unicode": "1F918-1F3FF",
+  "metal_tone5": {
+    "category": "people",
+    "moji": "🤘🏿",
+    "unicodeVersion": "8.0",
     "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
   },
-  {
-    "name": "metro",
-    "unicode": "1F687",
+  "metro": {
+    "category": "travel",
+    "moji": "🚇",
+    "unicodeVersion": "6.0",
     "digest": "b380247b61b5e2ca1b9b70fabff65907b2c3a5191a14b169ae094af94659b9b1"
   },
-  {
-    "name": "microphone",
-    "unicode": "1F3A4",
+  "microphone": {
+    "category": "activity",
+    "moji": "🎤",
+    "unicodeVersion": "6.0",
     "digest": "9ef4fc2e40d5391c4bb2d30f34f59662cff7cbb1b04341c9dac210d0e21b44ae"
   },
-  {
-    "name": "microphone2",
-    "unicode": "1F399",
+  "microphone2": {
+    "category": "objects",
+    "moji": "🎙",
+    "unicodeVersion": "7.0",
     "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
   },
-  {
-    "name": "studio_microphone",
-    "unicode": "1F399",
-    "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
-  },
-  {
-    "name": "microscope",
-    "unicode": "1F52C",
+  "microscope": {
+    "category": "objects",
+    "moji": "🔬",
+    "unicodeVersion": "6.0",
     "digest": "4ca4322c6ba99b8c15acdb8b605f84f87398769e504b262b134c1f3868b2692f"
   },
-  {
-    "name": "middle_finger",
-    "unicode": "1F595",
-    "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
-  },
-  {
-    "name": "reversed_hand_with_middle_finger_extended",
-    "unicode": "1F595",
+  "middle_finger": {
+    "category": "people",
+    "moji": "🖕",
+    "unicodeVersion": "7.0",
     "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
   },
-  {
-    "name": "middle_finger_tone1",
-    "unicode": "1F595-1F3FB",
+  "middle_finger_tone1": {
+    "category": "people",
+    "moji": "🖕🏻",
+    "unicodeVersion": "8.0",
     "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
   },
-  {
-    "name": "reversed_hand_with_middle_finger_extended_tone1",
-    "unicode": "1F595-1F3FB",
-    "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
-  },
-  {
-    "name": "middle_finger_tone2",
-    "unicode": "1F595-1F3FC",
-    "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
-  },
-  {
-    "name": "reversed_hand_with_middle_finger_extended_tone2",
-    "unicode": "1F595-1F3FC",
+  "middle_finger_tone2": {
+    "category": "people",
+    "moji": "🖕🏼",
+    "unicodeVersion": "8.0",
     "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
   },
-  {
-    "name": "middle_finger_tone3",
-    "unicode": "1F595-1F3FD",
+  "middle_finger_tone3": {
+    "category": "people",
+    "moji": "🖕🏽",
+    "unicodeVersion": "8.0",
     "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
   },
-  {
-    "name": "reversed_hand_with_middle_finger_extended_tone3",
-    "unicode": "1F595-1F3FD",
-    "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
-  },
-  {
-    "name": "middle_finger_tone4",
-    "unicode": "1F595-1F3FE",
-    "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
-  },
-  {
-    "name": "reversed_hand_with_middle_finger_extended_tone4",
-    "unicode": "1F595-1F3FE",
+  "middle_finger_tone4": {
+    "category": "people",
+    "moji": "🖕🏾",
+    "unicodeVersion": "8.0",
     "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
   },
-  {
-    "name": "middle_finger_tone5",
-    "unicode": "1F595-1F3FF",
+  "middle_finger_tone5": {
+    "category": "people",
+    "moji": "🖕🏿",
+    "unicodeVersion": "8.0",
     "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
   },
-  {
-    "name": "reversed_hand_with_middle_finger_extended_tone5",
-    "unicode": "1F595-1F3FF",
-    "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
-  },
-  {
-    "name": "military_medal",
-    "unicode": "1F396",
+  "military_medal": {
+    "category": "activity",
+    "moji": "🎖",
+    "unicodeVersion": "7.0",
     "digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d"
   },
-  {
-    "name": "milk",
-    "unicode": "1F95B",
-    "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
-  },
-  {
-    "name": "glass_of_milk",
-    "unicode": "1F95B",
+  "milk": {
+    "category": "food",
+    "moji": "🥛",
+    "unicodeVersion": "9.0",
     "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
   },
-  {
-    "name": "milky_way",
-    "unicode": "1F30C",
+  "milky_way": {
+    "category": "travel",
+    "moji": "🌌",
+    "unicodeVersion": "6.0",
     "digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d"
   },
-  {
-    "name": "minibus",
-    "unicode": "1F690",
+  "minibus": {
+    "category": "travel",
+    "moji": "🚐",
+    "unicodeVersion": "6.0",
     "digest": "08ccb4b1bf397b7c9aed901e2b5dcdd6cb8ca5c5487ef26775bb3120f7b92524"
   },
-  {
-    "name": "minidisc",
-    "unicode": "1F4BD",
+  "minidisc": {
+    "category": "objects",
+    "moji": "💽",
+    "unicodeVersion": "6.0",
     "digest": "bebf82c0b91ef66321e7ae7a0abf322e59b2f7d8e6fbf9a94243210c00229c59"
   },
-  {
-    "name": "mobile_phone_off",
-    "unicode": "1F4F4",
+  "mobile_phone_off": {
+    "category": "symbols",
+    "moji": "📴",
+    "unicodeVersion": "6.0",
     "digest": "6f9d8d6a32fc998f5d8144a5ff7e2ad00de37ad464cd97285e7c72efb09a1feb"
   },
-  {
-    "name": "money_mouth",
-    "unicode": "1F911",
+  "money_mouth": {
+    "category": "people",
+    "moji": "🤑",
+    "unicodeVersion": "8.0",
     "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
   },
-  {
-    "name": "money_mouth_face",
-    "unicode": "1F911",
-    "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
-  },
-  {
-    "name": "money_with_wings",
-    "unicode": "1F4B8",
+  "money_with_wings": {
+    "category": "objects",
+    "moji": "💸",
+    "unicodeVersion": "6.0",
     "digest": "15fcf0595021374ba091ca00efdb4167770da4d421eab930964108545f4edab9"
   },
-  {
-    "name": "moneybag",
-    "unicode": "1F4B0",
+  "moneybag": {
+    "category": "objects",
+    "moji": "💰",
+    "unicodeVersion": "6.0",
     "digest": "02d708e2f603b0df6f6c169b5c49b3452e1c02e7d72e96f228b73d0b0a20bff4"
   },
-  {
-    "name": "monkey",
-    "unicode": "1F412",
+  "monkey": {
+    "category": "nature",
+    "moji": "🐒",
+    "unicodeVersion": "6.0",
     "digest": "3588a544d6d9e9995b45d60327a1a42002fa1faa4d48224b140facd249af1c67"
   },
-  {
-    "name": "monkey_face",
-    "unicode": "1F435",
+  "monkey_face": {
+    "category": "nature",
+    "moji": "🐵",
+    "unicodeVersion": "6.0",
     "digest": "9e263ef5ca42bb76d1b1d1e3cbf020bcf05023a6e9f91301d30c9eb406363a2a"
   },
-  {
-    "name": "monorail",
-    "unicode": "1F69D",
+  "monorail": {
+    "category": "travel",
+    "moji": "🚝",
+    "unicodeVersion": "6.0",
     "digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad"
   },
-  {
-    "name": "mortar_board",
-    "unicode": "1F393",
+  "mortar_board": {
+    "category": "people",
+    "moji": "🎓",
+    "unicodeVersion": "6.0",
     "digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410"
   },
-  {
-    "name": "mosque",
-    "unicode": "1F54C",
+  "mosque": {
+    "category": "travel",
+    "moji": "🕌",
+    "unicodeVersion": "8.0",
     "digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196"
   },
-  {
-    "name": "motor_scooter",
-    "unicode": "1F6F5",
-    "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
-  },
-  {
-    "name": "motorbike",
-    "unicode": "1F6F5",
+  "motor_scooter": {
+    "category": "travel",
+    "moji": "🛵",
+    "unicodeVersion": "9.0",
     "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
   },
-  {
-    "name": "motorboat",
-    "unicode": "1F6E5",
+  "motorboat": {
+    "category": "travel",
+    "moji": "🛥",
+    "unicodeVersion": "7.0",
     "digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01"
   },
-  {
-    "name": "motorcycle",
-    "unicode": "1F3CD",
+  "motorcycle": {
+    "category": "travel",
+    "moji": "🏍",
+    "unicodeVersion": "7.0",
     "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
   },
-  {
-    "name": "racing_motorcycle",
-    "unicode": "1F3CD",
-    "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
-  },
-  {
-    "name": "motorway",
-    "unicode": "1F6E3",
+  "motorway": {
+    "category": "travel",
+    "moji": "🛣",
+    "unicodeVersion": "7.0",
     "digest": "148c3c13c7c4565453d16e504e0d4b8d007e4f2cad1ab56b1b51fefe39162d17"
   },
-  {
-    "name": "mount_fuji",
-    "unicode": "1F5FB",
+  "mount_fuji": {
+    "category": "travel",
+    "moji": "🗻",
+    "unicodeVersion": "6.0",
     "digest": "f8093b9dba62b22c6c88f137be88b2fd3971c560714db15ec053cf697a3820bc"
   },
-  {
-    "name": "mountain",
-    "unicode": "26F0",
+  "mountain": {
+    "category": "travel",
+    "moji": "⛰",
+    "unicodeVersion": "5.2",
     "digest": "07423804ad79da68f140948d29df193f5d5343b7b2c23758c086697c4d3a50da"
   },
-  {
-    "name": "mountain_bicyclist",
-    "unicode": "1F6B5",
+  "mountain_bicyclist": {
+    "category": "activity",
+    "moji": "🚵",
+    "unicodeVersion": "6.0",
     "digest": "91084b6c887cb7e34f3d7ec30656ecb82c36cc987f53a6c83ccb4c6f7950f96a"
   },
-  {
-    "name": "mountain_bicyclist_tone1",
-    "unicode": "1F6B5-1F3FB",
+  "mountain_bicyclist_tone1": {
+    "category": "activity",
+    "moji": "🚵🏻",
+    "unicodeVersion": "8.0",
     "digest": "5d57fcfad61bca26c3e8965eb57602a1993a3117ebdda0f24569af730310ab6e"
   },
-  {
-    "name": "mountain_bicyclist_tone2",
-    "unicode": "1F6B5-1F3FC",
+  "mountain_bicyclist_tone2": {
+    "category": "activity",
+    "moji": "🚵🏼",
+    "unicodeVersion": "8.0",
     "digest": "c0da7fb85d99aa01a665f64063cd7e2d994f8a16d3f6fbf52df5d471e771a98a"
   },
-  {
-    "name": "mountain_bicyclist_tone3",
-    "unicode": "1F6B5-1F3FD",
+  "mountain_bicyclist_tone3": {
+    "category": "activity",
+    "moji": "🚵🏽",
+    "unicodeVersion": "8.0",
     "digest": "b099e7ee84eae44ebc99023fa06bdf37ffa0d69767c7c0163a89f7ced2a26765"
   },
-  {
-    "name": "mountain_bicyclist_tone4",
-    "unicode": "1F6B5-1F3FE",
+  "mountain_bicyclist_tone4": {
+    "category": "activity",
+    "moji": "🚵🏾",
+    "unicodeVersion": "8.0",
     "digest": "9d09f7b3899ea44e736f237a161ef8d5170dccfa162a872c59532ceaf65ee007"
   },
-  {
-    "name": "mountain_bicyclist_tone5",
-    "unicode": "1F6B5-1F3FF",
+  "mountain_bicyclist_tone5": {
+    "category": "activity",
+    "moji": "🚵🏿",
+    "unicodeVersion": "8.0",
     "digest": "71e374981d955056748a60c6d1820b45e9688a156b55318b4ea54a3a67ca801c"
   },
-  {
-    "name": "mountain_cableway",
-    "unicode": "1F6A0",
+  "mountain_cableway": {
+    "category": "travel",
+    "moji": "🚠",
+    "unicodeVersion": "6.0",
     "digest": "e261c3292758b1c0063c5a0d0c7f5c9803306d2265e08677027e1210506ced94"
   },
-  {
-    "name": "mountain_railway",
-    "unicode": "1F69E",
+  "mountain_railway": {
+    "category": "travel",
+    "moji": "🚞",
+    "unicodeVersion": "6.0",
     "digest": "b0987f8f391b3cbc7a56b9b8945ebfca240e01d12f8fd163877ebebe51d6b277"
   },
-  {
-    "name": "mountain_snow",
-    "unicode": "1F3D4",
-    "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
-  },
-  {
-    "name": "snow_capped_mountain",
-    "unicode": "1F3D4",
+  "mountain_snow": {
+    "category": "travel",
+    "moji": "🏔",
+    "unicodeVersion": "7.0",
     "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
   },
-  {
-    "name": "mouse",
-    "unicode": "1F42D",
+  "mouse": {
+    "category": "nature",
+    "moji": "🐭",
+    "unicodeVersion": "6.0",
     "digest": "007dd108507b45224f7a1fad3c1de6ecc75f38d71fc142744611eb13555f5eff"
   },
-  {
-    "name": "mouse2",
-    "unicode": "1F401",
+  "mouse2": {
+    "category": "nature",
+    "moji": "🐁",
+    "unicodeVersion": "6.0",
     "digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d"
   },
-  {
-    "name": "mouse_three_button",
-    "unicode": "1F5B1",
+  "mouse_three_button": {
+    "category": "objects",
+    "moji": "🖱",
+    "unicodeVersion": "7.0",
     "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
   },
-  {
-    "name": "three_button_mouse",
-    "unicode": "1F5B1",
-    "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
-  },
-  {
-    "name": "movie_camera",
-    "unicode": "1F3A5",
+  "movie_camera": {
+    "category": "objects",
+    "moji": "🎥",
+    "unicodeVersion": "6.0",
     "digest": "f7e285eda35b4431c07951e071643ddc34147cd76640e0d516fbfd11208346e9"
   },
-  {
-    "name": "moyai",
-    "unicode": "1F5FF",
+  "moyai": {
+    "category": "objects",
+    "moji": "🗿",
+    "unicodeVersion": "6.0",
     "digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb"
   },
-  {
-    "name": "mrs_claus",
-    "unicode": "1F936",
-    "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
-  },
-  {
-    "name": "mother_christmas",
-    "unicode": "1F936",
+  "mrs_claus": {
+    "category": "people",
+    "moji": "🤶",
+    "unicodeVersion": "9.0",
     "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
   },
-  {
-    "name": "mrs_claus_tone1",
-    "unicode": "1F936-1F3FB",
+  "mrs_claus_tone1": {
+    "category": "people",
+    "moji": "🤶🏻",
+    "unicodeVersion": "9.0",
     "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
   },
-  {
-    "name": "mother_christmas_tone1",
-    "unicode": "1F936-1F3FB",
-    "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
-  },
-  {
-    "name": "mrs_claus_tone2",
-    "unicode": "1F936-1F3FC",
-    "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
-  },
-  {
-    "name": "mother_christmas_tone2",
-    "unicode": "1F936-1F3FC",
+  "mrs_claus_tone2": {
+    "category": "people",
+    "moji": "🤶🏼",
+    "unicodeVersion": "9.0",
     "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
   },
-  {
-    "name": "mrs_claus_tone3",
-    "unicode": "1F936-1F3FD",
+  "mrs_claus_tone3": {
+    "category": "people",
+    "moji": "🤶🏽",
+    "unicodeVersion": "9.0",
     "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
   },
-  {
-    "name": "mother_christmas_tone3",
-    "unicode": "1F936-1F3FD",
-    "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
-  },
-  {
-    "name": "mrs_claus_tone4",
-    "unicode": "1F936-1F3FE",
-    "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
-  },
-  {
-    "name": "mother_christmas_tone4",
-    "unicode": "1F936-1F3FE",
+  "mrs_claus_tone4": {
+    "category": "people",
+    "moji": "🤶🏾",
+    "unicodeVersion": "9.0",
     "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
   },
-  {
-    "name": "mrs_claus_tone5",
-    "unicode": "1F936-1F3FF",
+  "mrs_claus_tone5": {
+    "category": "people",
+    "moji": "🤶🏿",
+    "unicodeVersion": "9.0",
     "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
   },
-  {
-    "name": "mother_christmas_tone5",
-    "unicode": "1F936-1F3FF",
-    "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
-  },
-  {
-    "name": "muscle",
-    "unicode": "1F4AA",
+  "muscle": {
+    "category": "people",
+    "moji": "💪",
+    "unicodeVersion": "6.0",
     "digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba"
   },
-  {
-    "name": "muscle_tone1",
-    "unicode": "1F4AA-1F3FB",
+  "muscle_tone1": {
+    "category": "people",
+    "moji": "💪🏻",
+    "unicodeVersion": "8.0",
     "digest": "4a2fa226a05bb847b62cdd163eb6c2d514d3c2330a727991cf550c0d32b0e818"
   },
-  {
-    "name": "muscle_tone2",
-    "unicode": "1F4AA-1F3FC",
+  "muscle_tone2": {
+    "category": "people",
+    "moji": "💪🏼",
+    "unicodeVersion": "8.0",
     "digest": "a8d5ecce335c782ca5f5e55763c06cfefa1c16c24cd6602237cf125d4ff95e47"
   },
-  {
-    "name": "muscle_tone3",
-    "unicode": "1F4AA-1F3FD",
+  "muscle_tone3": {
+    "category": "people",
+    "moji": "💪🏽",
+    "unicodeVersion": "8.0",
     "digest": "070354b443faec3969663b770545fc4cf5ec75148557b2b9d6fc82ab22b43bd1"
   },
-  {
-    "name": "muscle_tone4",
-    "unicode": "1F4AA-1F3FE",
+  "muscle_tone4": {
+    "category": "people",
+    "moji": "💪🏾",
+    "unicodeVersion": "8.0",
     "digest": "8eafcdb6a607aeafa673c257df0d2a1b20f00fc0868d811babcbe784490a0dd3"
   },
-  {
-    "name": "muscle_tone5",
-    "unicode": "1F4AA-1F3FF",
+  "muscle_tone5": {
+    "category": "people",
+    "moji": "💪🏿",
+    "unicodeVersion": "8.0",
     "digest": "85a1e2b5c89907694240e9c5b9d876a741fa7ba38918c5718273e289cbc40efe"
   },
-  {
-    "name": "mushroom",
-    "unicode": "1F344",
+  "mushroom": {
+    "category": "nature",
+    "moji": "🍄",
+    "unicodeVersion": "6.0",
     "digest": "aaca8cf7c5cfa4487b5fef365a231f98be4bbf041197fc022161bcc8ce6f57c8"
   },
-  {
-    "name": "musical_keyboard",
-    "unicode": "1F3B9",
+  "musical_keyboard": {
+    "category": "activity",
+    "moji": "🎹",
+    "unicodeVersion": "6.0",
     "digest": "fb0a726728900377d76d94aac9c94dce29107e8e3f1dcb0599d95bce7169b492"
   },
-  {
-    "name": "musical_note",
-    "unicode": "1F3B5",
+  "musical_note": {
+    "category": "symbols",
+    "moji": "🎵",
+    "unicodeVersion": "6.0",
     "digest": "41288e79b4070bb980281d0e0d1c14d8b144b4aedb2eaadb9f2bebcb4ef892b4"
   },
-  {
-    "name": "musical_score",
-    "unicode": "1F3BC",
+  "musical_score": {
+    "category": "activity",
+    "moji": "🎼",
+    "unicodeVersion": "6.0",
     "digest": "f0f91b9fa4a2bff7a5a1a11afa6f31cfe7e5fa8b0d6f3cce904b781a28ed0277"
   },
-  {
-    "name": "mute",
-    "unicode": "1F507",
+  "mute": {
+    "category": "symbols",
+    "moji": "🔇",
+    "unicodeVersion": "6.0",
     "digest": "def277da49d744b55c7cdde269a15aa05315898f615e721ee7e9205d7b8030d6"
   },
-  {
-    "name": "nail_care",
-    "unicode": "1F485",
+  "nail_care": {
+    "category": "people",
+    "moji": "💅",
+    "unicodeVersion": "6.0",
     "digest": "48b33b1dbbd25b4f34ab2ca07bb99ddaaaa741990142c5623310f76b78c076f9"
   },
-  {
-    "name": "nail_care_tone1",
-    "unicode": "1F485-1F3FB",
+  "nail_care_tone1": {
+    "category": "people",
+    "moji": "💅🏻",
+    "unicodeVersion": "8.0",
     "digest": "a9ac92a34f407e7dd7c71377e6275e66657f7f42e4b911c540d1a66a02d92ac5"
   },
-  {
-    "name": "nail_care_tone2",
-    "unicode": "1F485-1F3FC",
+  "nail_care_tone2": {
+    "category": "people",
+    "moji": "💅🏼",
+    "unicodeVersion": "8.0",
     "digest": "f295ec85980aaa75818fad619c3d25042146ecbbf361db9e9bb96e7bc202bc73"
   },
-  {
-    "name": "nail_care_tone3",
-    "unicode": "1F485-1F3FD",
+  "nail_care_tone3": {
+    "category": "people",
+    "moji": "💅🏽",
+    "unicodeVersion": "8.0",
     "digest": "02ec373052a250977298bae85262177910126cc10de9480f1afa328ac2f65a95"
   },
-  {
-    "name": "nail_care_tone4",
-    "unicode": "1F485-1F3FE",
+  "nail_care_tone4": {
+    "category": "people",
+    "moji": "💅🏾",
+    "unicodeVersion": "8.0",
     "digest": "f3d95390ab59caedfda66122bbd0acf3aabedc142fc48352d68900766a7e6f5c"
   },
-  {
-    "name": "nail_care_tone5",
-    "unicode": "1F485-1F3FF",
+  "nail_care_tone5": {
+    "category": "people",
+    "moji": "💅🏿",
+    "unicodeVersion": "8.0",
     "digest": "009423c97f2aafd24fb8c7c485c58b30bbf9ae6797cc14b80d472b207327b518"
   },
-  {
-    "name": "name_badge",
-    "unicode": "1F4DB",
+  "name_badge": {
+    "category": "symbols",
+    "moji": "📛",
+    "unicodeVersion": "6.0",
     "digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628"
   },
-  {
-    "name": "nauseated_face",
-    "unicode": "1F922",
-    "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
-  },
-  {
-    "name": "sick",
-    "unicode": "1F922",
+  "nauseated_face": {
+    "category": "people",
+    "moji": "🤢",
+    "unicodeVersion": "9.0",
     "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
   },
-  {
-    "name": "necktie",
-    "unicode": "1F454",
+  "necktie": {
+    "category": "people",
+    "moji": "👔",
+    "unicodeVersion": "6.0",
     "digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68"
   },
-  {
-    "name": "negative_squared_cross_mark",
-    "unicode": "274E",
+  "negative_squared_cross_mark": {
+    "category": "symbols",
+    "moji": "❎",
+    "unicodeVersion": "6.0",
     "digest": "1cdaf4abc9adafa089c91c2e33a24e9e647aea0f857e767941a899a16ec53b74"
   },
-  {
-    "name": "nerd",
-    "unicode": "1F913",
+  "nerd": {
+    "category": "people",
+    "moji": "🤓",
+    "unicodeVersion": "8.0",
     "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
   },
-  {
-    "name": "nerd_face",
-    "unicode": "1F913",
-    "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
-  },
-  {
-    "name": "neutral_face",
-    "unicode": "1F610",
+  "neutral_face": {
+    "category": "people",
+    "moji": "😐",
+    "unicodeVersion": "6.0",
     "digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e"
   },
-  {
-    "name": "new",
-    "unicode": "1F195",
+  "new": {
+    "category": "symbols",
+    "moji": "🆕",
+    "unicodeVersion": "6.0",
     "digest": "e20bc3e9f40726afd0cfb7268d02f1e1a07343364fd08b252d59f38de067bf06"
   },
-  {
-    "name": "new_moon",
-    "unicode": "1F311",
+  "new_moon": {
+    "category": "nature",
+    "moji": "🌑",
+    "unicodeVersion": "6.0",
     "digest": "dbfc5dcae34b45f15ff767e297cba3a12cb83f3b542db8cfc8dbd9669e0df46c"
   },
-  {
-    "name": "new_moon_with_face",
-    "unicode": "1F31A",
+  "new_moon_with_face": {
+    "category": "nature",
+    "moji": "🌚",
+    "unicodeVersion": "6.0",
     "digest": "c66d347d2222ac8d77d323a07699aff6b168328648db4f885b1ed0e2831fd59b"
   },
-  {
-    "name": "newspaper",
-    "unicode": "1F4F0",
+  "newspaper": {
+    "category": "objects",
+    "moji": "📰",
+    "unicodeVersion": "6.0",
     "digest": "c05e986d9cdac11afa30c6a21a72572ddf50fc64e87ae0c4e0ad57ffe70acc5c"
   },
-  {
-    "name": "newspaper2",
-    "unicode": "1F5DE",
-    "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
-  },
-  {
-    "name": "rolled_up_newspaper",
-    "unicode": "1F5DE",
+  "newspaper2": {
+    "category": "objects",
+    "moji": "🗞",
+    "unicodeVersion": "7.0",
     "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
   },
-  {
-    "name": "ng",
-    "unicode": "1F196",
+  "ng": {
+    "category": "symbols",
+    "moji": "🆖",
+    "unicodeVersion": "6.0",
     "digest": "34d5a11c70f48ea719e602908534f446b192622e775d4160f0e1ec52c342a35c"
   },
-  {
-    "name": "night_with_stars",
-    "unicode": "1F303",
+  "night_with_stars": {
+    "category": "travel",
+    "moji": "🌃",
+    "unicodeVersion": "6.0",
     "digest": "39d9c079be80ee6ce1667531be528a2aa7f8bd46c7b6c2a6ee279d9a207c84a4"
   },
-  {
-    "name": "nine",
-    "unicode": "0039-20E3",
+  "nine": {
+    "category": "symbols",
+    "moji": "9️⃣",
+    "unicodeVersion": "3.0",
     "digest": "8bb40750eda8506ef877c9a3b8e2039d26f20eef345742f635740574a7e8daa6"
   },
-  {
-    "name": "no_bell",
-    "unicode": "1F515",
+  "no_bell": {
+    "category": "symbols",
+    "moji": "🔕",
+    "unicodeVersion": "6.0",
     "digest": "6542a9a5656c79c153f8c37f12d48f677c89b02ed0989ae37fa5e51ce6895422"
   },
-  {
-    "name": "no_bicycles",
-    "unicode": "1F6B3",
+  "no_bicycles": {
+    "category": "symbols",
+    "moji": "🚳",
+    "unicodeVersion": "6.0",
     "digest": "af71c183545da2ff4c05609f9d572edb64b63ccba7c6a4b208d271558aa92b0a"
   },
-  {
-    "name": "no_entry",
-    "unicode": "26D4",
+  "no_entry": {
+    "category": "symbols",
+    "moji": "⛔",
+    "unicodeVersion": "5.2",
     "digest": "dc0bac1ed9ab8e9af143f0fce5043fe68f7f46bd80856cdec95d20c3999b637d"
   },
-  {
-    "name": "no_entry_sign",
-    "unicode": "1F6AB",
+  "no_entry_sign": {
+    "category": "symbols",
+    "moji": "🚫",
+    "unicodeVersion": "6.0",
     "digest": "2c1fceef23b62effca68e0e087b8f020125d25b98d61492b1540055d1914fdc3"
   },
-  {
-    "name": "no_good",
-    "unicode": "1F645",
+  "no_good": {
+    "category": "people",
+    "moji": "🙅",
+    "unicodeVersion": "6.0",
     "digest": "6eb970b104389be5d18657d7c04be5149958c26855c52ea68574af852c5f85c4"
   },
-  {
-    "name": "no_good_tone1",
-    "unicode": "1F645-1F3FB",
+  "no_good_tone1": {
+    "category": "people",
+    "moji": "🙅🏻",
+    "unicodeVersion": "8.0",
     "digest": "c20a24a1e536240b4dcf90ecb530796de621d7ba1fb9e3fa0f849d048c509c03"
   },
-  {
-    "name": "no_good_tone2",
-    "unicode": "1F645-1F3FC",
+  "no_good_tone2": {
+    "category": "people",
+    "moji": "🙅🏼",
+    "unicodeVersion": "8.0",
     "digest": "f31a4628c1f2e6a39288fda8eb19c9ec89983e3726e17a09384d9ecc13ef0b4c"
   },
-  {
-    "name": "no_good_tone3",
-    "unicode": "1F645-1F3FD",
+  "no_good_tone3": {
+    "category": "people",
+    "moji": "🙅🏽",
+    "unicodeVersion": "8.0",
     "digest": "959dec1bfdaf37b20a86ab2bcbdbacd3179c87b163042377d966eab47564c0fb"
   },
-  {
-    "name": "no_good_tone4",
-    "unicode": "1F645-1F3FE",
+  "no_good_tone4": {
+    "category": "people",
+    "moji": "🙅🏾",
+    "unicodeVersion": "8.0",
     "digest": "efd931f0080adf2e04129c83a8b24fda0ae7a9fa7c4b463686c0b99023620db8"
   },
-  {
-    "name": "no_good_tone5",
-    "unicode": "1F645-1F3FF",
+  "no_good_tone5": {
+    "category": "people",
+    "moji": "🙅🏿",
+    "unicodeVersion": "8.0",
     "digest": "f35df2b26af9baef47c1f8cc97a1b28a58aa7fcb2a13fdac7b2d9189f1e40105"
   },
-  {
-    "name": "no_mobile_phones",
-    "unicode": "1F4F5",
+  "no_mobile_phones": {
+    "category": "symbols",
+    "moji": "📵",
+    "unicodeVersion": "6.0",
     "digest": "a472decd6ac7f9777961c09e00458746b2c04965585e3bee4556be3968e55bcd"
   },
-  {
-    "name": "no_mouth",
-    "unicode": "1F636",
+  "no_mouth": {
+    "category": "people",
+    "moji": "😶",
+    "unicodeVersion": "6.0",
     "digest": "72dda8b1e3ad4b05d9b095f9bd05e95d7ba013906c68914976a4554e8edf5866"
   },
-  {
-    "name": "no_pedestrians",
-    "unicode": "1F6B7",
+  "no_pedestrians": {
+    "category": "symbols",
+    "moji": "🚷",
+    "unicodeVersion": "6.0",
     "digest": "062b4a71b338fe09775e465bfba8ac04efbb3640330e8cabe88f3af62b0f4225"
   },
-  {
-    "name": "no_smoking",
-    "unicode": "1F6AD",
+  "no_smoking": {
+    "category": "symbols",
+    "moji": "🚭",
+    "unicodeVersion": "6.0",
     "digest": "ae2ebb331f79f6074091c0ee9cd69fce16d5e12a131d18973fc05520097e14ee"
   },
-  {
-    "name": "non-potable_water",
-    "unicode": "1F6B1",
+  "non-potable_water": {
+    "category": "symbols",
+    "moji": "🚱",
+    "unicodeVersion": "6.0",
     "digest": "32eba0a99b498133c2e4450036f768d3dccaaf5b50adc9ad988757adc777a6a1"
   },
-  {
-    "name": "nose",
-    "unicode": "1F443",
+  "nose": {
+    "category": "people",
+    "moji": "👃",
+    "unicodeVersion": "6.0",
     "digest": "9f800e24658ea3cebe1144d5d808cf13a88261f1a7f1f81a10d03b3d9d00e541"
   },
-  {
-    "name": "nose_tone1",
-    "unicode": "1F443-1F3FB",
+  "nose_tone1": {
+    "category": "people",
+    "moji": "👃🏻",
+    "unicodeVersion": "8.0",
     "digest": "a2d0af22284b1d264eb780943b8360f463996a5c9c9584b8473edf8d442d9173"
   },
-  {
-    "name": "nose_tone2",
-    "unicode": "1F443-1F3FC",
+  "nose_tone2": {
+    "category": "people",
+    "moji": "👃🏼",
+    "unicodeVersion": "8.0",
     "digest": "244dcaa8540024cf521f29f36bd48f933bf82f4833e35e6fa0abf113022038f3"
   },
-  {
-    "name": "nose_tone3",
-    "unicode": "1F443-1F3FD",
+  "nose_tone3": {
+    "category": "people",
+    "moji": "👃🏽",
+    "unicodeVersion": "8.0",
     "digest": "c935b64866f0d49da52035aa09f36ff56d238eb7f5b92205386451056e8ea74f"
   },
-  {
-    "name": "nose_tone4",
-    "unicode": "1F443-1F3FE",
+  "nose_tone4": {
+    "category": "people",
+    "moji": "👃🏾",
+    "unicodeVersion": "8.0",
     "digest": "a87e95fd9319c49e66b6dea0e57319d0ed9921b8d94df037767bf3d5dc7c94f3"
   },
-  {
-    "name": "nose_tone5",
-    "unicode": "1F443-1F3FF",
+  "nose_tone5": {
+    "category": "people",
+    "moji": "👃🏿",
+    "unicodeVersion": "8.0",
     "digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60"
   },
-  {
-    "name": "notebook",
-    "unicode": "1F4D3",
+  "notebook": {
+    "category": "objects",
+    "moji": "📓",
+    "unicodeVersion": "6.0",
     "digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8"
   },
-  {
-    "name": "notebook_with_decorative_cover",
-    "unicode": "1F4D4",
+  "notebook_with_decorative_cover": {
+    "category": "objects",
+    "moji": "📔",
+    "unicodeVersion": "6.0",
     "digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef"
   },
-  {
-    "name": "notepad_spiral",
-    "unicode": "1F5D2",
+  "notepad_spiral": {
+    "category": "objects",
+    "moji": "🗒",
+    "unicodeVersion": "7.0",
     "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
   },
-  {
-    "name": "spiral_note_pad",
-    "unicode": "1F5D2",
-    "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
-  },
-  {
-    "name": "notes",
-    "unicode": "1F3B6",
+  "notes": {
+    "category": "symbols",
+    "moji": "🎶",
+    "unicodeVersion": "6.0",
     "digest": "98467e0adc134d45676ef1c6c459e5853a9db50c8a6e91b6aec7d449aa737f48"
   },
-  {
-    "name": "nut_and_bolt",
-    "unicode": "1F529",
+  "nut_and_bolt": {
+    "category": "objects",
+    "moji": "🔩",
+    "unicodeVersion": "6.0",
     "digest": "a77bd72f29a7302195dcec240174b15586de79e3204258e3fb401a6ea90563b3"
   },
-  {
-    "name": "o",
-    "unicode": "2B55",
+  "o": {
+    "category": "symbols",
+    "moji": "⭕",
+    "unicodeVersion": "5.2",
     "digest": "2387e5fd9ae4c2972d40298d32319b8fa55c50dbfc1c04c5c36088213e6951dd"
   },
-  {
-    "name": "o2",
-    "unicode": "1F17E",
+  "o2": {
+    "category": "symbols",
+    "moji": "🅾",
+    "unicodeVersion": "6.0",
     "digest": "6a9ccb0bf394e4d05ffda19327cee18f7b9ed80367fc7f41c93da9bb7efab0bf"
   },
-  {
-    "name": "ocean",
-    "unicode": "1F30A",
+  "ocean": {
+    "category": "nature",
+    "moji": "🌊",
+    "unicodeVersion": "6.0",
     "digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e"
   },
-  {
-    "name": "octagonal_sign",
-    "unicode": "1F6D1",
-    "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
-  },
-  {
-    "name": "stop_sign",
-    "unicode": "1F6D1",
+  "octagonal_sign": {
+    "category": "symbols",
+    "moji": "🛑",
+    "unicodeVersion": "9.0",
     "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
   },
-  {
-    "name": "octopus",
-    "unicode": "1F419",
+  "octopus": {
+    "category": "nature",
+    "moji": "🐙",
+    "unicodeVersion": "6.0",
     "digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59"
   },
-  {
-    "name": "oden",
-    "unicode": "1F362",
+  "oden": {
+    "category": "food",
+    "moji": "🍢",
+    "unicodeVersion": "6.0",
     "digest": "089974cb13a0bef6a245fc73029c5ed5153fd4caae0177b835f779e32200b8aa"
   },
-  {
-    "name": "office",
-    "unicode": "1F3E2",
+  "office": {
+    "category": "travel",
+    "moji": "🏢",
+    "unicodeVersion": "6.0",
     "digest": "3633a2e91036362e273eef4e0cfbdbbb4cb1208afe2cfa110ebef7b78109a66f"
   },
-  {
-    "name": "oil",
-    "unicode": "1F6E2",
+  "oil": {
+    "category": "objects",
+    "moji": "🛢",
+    "unicodeVersion": "7.0",
     "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
   },
-  {
-    "name": "oil_drum",
-    "unicode": "1F6E2",
-    "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
-  },
-  {
-    "name": "ok",
-    "unicode": "1F197",
+  "ok": {
+    "category": "symbols",
+    "moji": "🆗",
+    "unicodeVersion": "6.0",
     "digest": "5f320f9b96e98a2f17ebe240daff9b9fd2ae0727cd6c8e4633b1744356e89365"
   },
-  {
-    "name": "ok_hand",
-    "unicode": "1F44C",
+  "ok_hand": {
+    "category": "people",
+    "moji": "👌",
+    "unicodeVersion": "6.0",
     "digest": "d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d"
   },
-  {
-    "name": "ok_hand_tone1",
-    "unicode": "1F44C-1F3FB",
+  "ok_hand_tone1": {
+    "category": "people",
+    "moji": "👌🏻",
+    "unicodeVersion": "8.0",
     "digest": "ef1508efcf483b09807554fe0e451c2948224f9deb85463e8e0dad6875b54012"
   },
-  {
-    "name": "ok_hand_tone2",
-    "unicode": "1F44C-1F3FC",
+  "ok_hand_tone2": {
+    "category": "people",
+    "moji": "👌🏼",
+    "unicodeVersion": "8.0",
     "digest": "1215a101a082fd8e04c5d2f7e3c59d0f480cb0bedd79aeab5d36676bfe760088"
   },
-  {
-    "name": "ok_hand_tone3",
-    "unicode": "1F44C-1F3FD",
+  "ok_hand_tone3": {
+    "category": "people",
+    "moji": "👌🏽",
+    "unicodeVersion": "8.0",
     "digest": "6fe0ed9fb42e86bb2bed4cb37b2acacacda1471fb1ee845ad55e54fb0897fbf4"
   },
-  {
-    "name": "ok_hand_tone4",
-    "unicode": "1F44C-1F3FE",
+  "ok_hand_tone4": {
+    "category": "people",
+    "moji": "👌🏾",
+    "unicodeVersion": "8.0",
     "digest": "bfb9041c49d95e901a667264abaf9b398f6c4aa8b52bf5191c122db20c13c020"
   },
-  {
-    "name": "ok_hand_tone5",
-    "unicode": "1F44C-1F3FF",
+  "ok_hand_tone5": {
+    "category": "people",
+    "moji": "👌🏿",
+    "unicodeVersion": "8.0",
     "digest": "1c218dc04d698da2cbdd7bea1ca3f845f9b386e967b7247c52f4b0f6ec8f5320"
   },
-  {
-    "name": "ok_woman",
-    "unicode": "1F646",
+  "ok_woman": {
+    "category": "people",
+    "moji": "🙆",
+    "unicodeVersion": "6.0",
     "digest": "3f8bd4ce2c4497155d697e5a71ebdc9339f65633d07fa9a7903e1bd76cfa4ba1"
   },
-  {
-    "name": "ok_woman_tone1",
-    "unicode": "1F646-1F3FB",
+  "ok_woman_tone1": {
+    "category": "people",
+    "moji": "🙆🏻",
+    "unicodeVersion": "8.0",
     "digest": "1660cd904ccd2ecdc6f4ba00527f7d4ec8c33f3c6183344616f97badae4c3730"
   },
-  {
-    "name": "ok_woman_tone2",
-    "unicode": "1F646-1F3FC",
+  "ok_woman_tone2": {
+    "category": "people",
+    "moji": "🙆🏼",
+    "unicodeVersion": "8.0",
     "digest": "7ba5fddd1e141424fac6778894dfc5af28e125839c58937c69496f99cd2c4002"
   },
-  {
-    "name": "ok_woman_tone3",
-    "unicode": "1F646-1F3FD",
+  "ok_woman_tone3": {
+    "category": "people",
+    "moji": "🙆🏽",
+    "unicodeVersion": "8.0",
     "digest": "1d972b8377c52f598406f59ab1e5be41aaf8f027e1fefba3deda66312ccd6a9b"
   },
-  {
-    "name": "ok_woman_tone4",
-    "unicode": "1F646-1F3FE",
+  "ok_woman_tone4": {
+    "category": "people",
+    "moji": "🙆🏾",
+    "unicodeVersion": "8.0",
     "digest": "a176328d8f53503aa743448968afd21d72ffd3510555526a3fb38d6b30ee7c15"
   },
-  {
-    "name": "ok_woman_tone5",
-    "unicode": "1F646-1F3FF",
+  "ok_woman_tone5": {
+    "category": "people",
+    "moji": "🙆🏿",
+    "unicodeVersion": "8.0",
     "digest": "13cfc1b589c57e81f768ee07a14b737cafc71407a7eb0956728b2ec4b1df14c4"
   },
-  {
-    "name": "older_man",
-    "unicode": "1F474",
+  "older_man": {
+    "category": "people",
+    "moji": "👴",
+    "unicodeVersion": "6.0",
     "digest": "4c0462b199bf26181c9e4d2d4cb878a32b0294566941212efc67362d0645f948"
   },
-  {
-    "name": "older_man_tone1",
-    "unicode": "1F474-1F3FB",
+  "older_man_tone1": {
+    "category": "people",
+    "moji": "👴🏻",
+    "unicodeVersion": "8.0",
     "digest": "99baa083f78cb01166d0a928d0b53682be14be04c29fc17bef14aac1a73a61e6"
   },
-  {
-    "name": "older_man_tone2",
-    "unicode": "1F474-1F3FC",
+  "older_man_tone2": {
+    "category": "people",
+    "moji": "👴🏼",
+    "unicodeVersion": "8.0",
     "digest": "5b4ce713e8820ba517fe92c25f3b93e6a6bf3704d1f982c461d5f31fc02b9d3d"
   },
-  {
-    "name": "older_man_tone3",
-    "unicode": "1F474-1F3FD",
+  "older_man_tone3": {
+    "category": "people",
+    "moji": "👴🏽",
+    "unicodeVersion": "8.0",
     "digest": "0eff72b3226c3a703c635798ee84129a695c896fa011fe1adbc105312eecc083"
   },
-  {
-    "name": "older_man_tone4",
-    "unicode": "1F474-1F3FE",
+  "older_man_tone4": {
+    "category": "people",
+    "moji": "👴🏾",
+    "unicodeVersion": "8.0",
     "digest": "ad9ba82b0c5d3b171b0639ee4265370dbddff5e0eeb70729db122659bb8c8f84"
   },
-  {
-    "name": "older_man_tone5",
-    "unicode": "1F474-1F3FF",
+  "older_man_tone5": {
+    "category": "people",
+    "moji": "👴🏿",
+    "unicodeVersion": "8.0",
     "digest": "5eb0a7467cc40e75752e11fd5126b275863dc037557a0d0d3b24b681e00c2386"
   },
-  {
-    "name": "older_woman",
-    "unicode": "1F475",
-    "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
-  },
-  {
-    "name": "grandma",
-    "unicode": "1F475",
+  "older_woman": {
+    "category": "people",
+    "moji": "👵",
+    "unicodeVersion": "6.0",
     "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
   },
-  {
-    "name": "older_woman_tone1",
-    "unicode": "1F475-1F3FB",
+  "older_woman_tone1": {
+    "category": "people",
+    "moji": "👵🏻",
+    "unicodeVersion": "8.0",
     "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
   },
-  {
-    "name": "grandma_tone1",
-    "unicode": "1F475-1F3FB",
-    "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
-  },
-  {
-    "name": "older_woman_tone2",
-    "unicode": "1F475-1F3FC",
-    "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
-  },
-  {
-    "name": "grandma_tone2",
-    "unicode": "1F475-1F3FC",
+  "older_woman_tone2": {
+    "category": "people",
+    "moji": "👵🏼",
+    "unicodeVersion": "8.0",
     "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
   },
-  {
-    "name": "older_woman_tone3",
-    "unicode": "1F475-1F3FD",
+  "older_woman_tone3": {
+    "category": "people",
+    "moji": "👵🏽",
+    "unicodeVersion": "8.0",
     "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
   },
-  {
-    "name": "grandma_tone3",
-    "unicode": "1F475-1F3FD",
-    "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
-  },
-  {
-    "name": "older_woman_tone4",
-    "unicode": "1F475-1F3FE",
-    "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
-  },
-  {
-    "name": "grandma_tone4",
-    "unicode": "1F475-1F3FE",
+  "older_woman_tone4": {
+    "category": "people",
+    "moji": "👵🏾",
+    "unicodeVersion": "8.0",
     "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
   },
-  {
-    "name": "older_woman_tone5",
-    "unicode": "1F475-1F3FF",
+  "older_woman_tone5": {
+    "category": "people",
+    "moji": "👵🏿",
+    "unicodeVersion": "8.0",
     "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
   },
-  {
-    "name": "grandma_tone5",
-    "unicode": "1F475-1F3FF",
-    "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
-  },
-  {
-    "name": "om_symbol",
-    "unicode": "1F549",
+  "om_symbol": {
+    "category": "symbols",
+    "moji": "🕉",
+    "unicodeVersion": "7.0",
     "digest": "5ead73bea546ba9ba6da522f7280cc289c75ff5467742bdba31f92d0e1b3f4e6"
   },
-  {
-    "name": "on",
-    "unicode": "1F51B",
+  "on": {
+    "category": "symbols",
+    "moji": "🔛",
+    "unicodeVersion": "6.0",
     "digest": "9cc61a6b31a30c32dab594191bf23f91e341c4105384ab22158a6d43e6364631"
   },
-  {
-    "name": "oncoming_automobile",
-    "unicode": "1F698",
+  "oncoming_automobile": {
+    "category": "travel",
+    "moji": "🚘",
+    "unicodeVersion": "6.0",
     "digest": "557c9cacdc3f95215d4f7a6f097a2baa7c007cb9c519492a6717077af4ca6b56"
   },
-  {
-    "name": "oncoming_bus",
-    "unicode": "1F68D",
+  "oncoming_bus": {
+    "category": "travel",
+    "moji": "🚍",
+    "unicodeVersion": "6.0",
     "digest": "059f28ce6bfb337e107db5982cbd2004844450ef20b4a54b9ca3cb738360ab05"
   },
-  {
-    "name": "oncoming_police_car",
-    "unicode": "1F694",
+  "oncoming_police_car": {
+    "category": "travel",
+    "moji": "🚔",
+    "unicodeVersion": "6.0",
     "digest": "aee79306a0d129cfc1980f58db80391eb46d2d7d5f814bf431414dc7680cab72"
   },
-  {
-    "name": "oncoming_taxi",
-    "unicode": "1F696",
+  "oncoming_taxi": {
+    "category": "travel",
+    "moji": "🚖",
+    "unicodeVersion": "6.0",
     "digest": "84351489fc86d980b8d3eb9ec4e81120fe700b3ac01346daebe2b7aeb9607a55"
   },
-  {
-    "name": "one",
-    "unicode": "0031-20E3",
+  "one": {
+    "category": "symbols",
+    "moji": "1️⃣",
+    "unicodeVersion": "3.0",
     "digest": "d5d3fff04e68a114ff6464ee06fc831f3f381713045165f62a88d5e8215c195b"
   },
-  {
-    "name": "open_file_folder",
-    "unicode": "1F4C2",
+  "open_file_folder": {
+    "category": "objects",
+    "moji": "📂",
+    "unicodeVersion": "6.0",
     "digest": "96cfc322ee4903ae8cec07604811742245fd7d14f00bb70276d39d29c48bed28"
   },
-  {
-    "name": "open_hands",
-    "unicode": "1F450",
+  "open_hands": {
+    "category": "people",
+    "moji": "👐",
+    "unicodeVersion": "6.0",
     "digest": "a6c131da2040b48103cea14f280e728675da50fa448d2b3f3438fcbb5bf5596a"
   },
-  {
-    "name": "open_hands_tone1",
-    "unicode": "1F450-1F3FB",
+  "open_hands_tone1": {
+    "category": "people",
+    "moji": "👐🏻",
+    "unicodeVersion": "8.0",
     "digest": "867128dff2fa9b860c10c6b792f989f0c057928783696062378f834c0ef89d85"
   },
-  {
-    "name": "open_hands_tone2",
-    "unicode": "1F450-1F3FC",
+  "open_hands_tone2": {
+    "category": "people",
+    "moji": "👐🏼",
+    "unicodeVersion": "8.0",
     "digest": "487ff2745b03d49bb3b1d0acd86ba530fd8cc3f467ca3fa504f88f0ef1cbbc01"
   },
-  {
-    "name": "open_hands_tone3",
-    "unicode": "1F450-1F3FD",
+  "open_hands_tone3": {
+    "category": "people",
+    "moji": "👐🏽",
+    "unicodeVersion": "8.0",
     "digest": "cb8cddc8b8661f874ac9478289d16cc41406b947bb87f3363df518a588a53e16"
   },
-  {
-    "name": "open_hands_tone4",
-    "unicode": "1F450-1F3FE",
+  "open_hands_tone4": {
+    "category": "people",
+    "moji": "👐🏾",
+    "unicodeVersion": "8.0",
     "digest": "17dcc2c07230846a769f3c79ce618a757c88b9b58c95c6c5b2d7f968814d447d"
   },
-  {
-    "name": "open_hands_tone5",
-    "unicode": "1F450-1F3FF",
+  "open_hands_tone5": {
+    "category": "people",
+    "moji": "👐🏿",
+    "unicodeVersion": "8.0",
     "digest": "36b2493d67c84cea4f3f85a3088c6abcfd35cf99f7aeaeedfafa420ee878e3d2"
   },
-  {
-    "name": "open_mouth",
-    "unicode": "1F62E",
+  "open_mouth": {
+    "category": "people",
+    "moji": "😮",
+    "unicodeVersion": "6.1",
     "digest": "1906c5100ae0c8326ca5c4f9422976958a38dadd8d77724d68538a25d9623035"
   },
-  {
-    "name": "ophiuchus",
-    "unicode": "26CE",
+  "ophiuchus": {
+    "category": "symbols",
+    "moji": "⛎",
+    "unicodeVersion": "6.0",
     "digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b"
   },
-  {
-    "name": "orange_book",
-    "unicode": "1F4D9",
+  "orange_book": {
+    "category": "objects",
+    "moji": "📙",
+    "unicodeVersion": "6.0",
     "digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf"
   },
-  {
-    "name": "orthodox_cross",
-    "unicode": "2626",
+  "orthodox_cross": {
+    "category": "symbols",
+    "moji": "☦",
+    "unicodeVersion": "1.1",
     "digest": "c16372102f0169dd6d32eb2b27a633aaee74e4e0fddcf723c15ad97f9dc6075c"
   },
-  {
-    "name": "outbox_tray",
-    "unicode": "1F4E4",
+  "outbox_tray": {
+    "category": "objects",
+    "moji": "📤",
+    "unicodeVersion": "6.0",
     "digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf"
   },
-  {
-    "name": "owl",
-    "unicode": "1F989",
+  "owl": {
+    "category": "nature",
+    "moji": "🦉",
+    "unicodeVersion": "9.0",
     "digest": "f62ec1ad23ad9038966eea8d8b79660ac212f291af2e89bcdb0fdc683caf41e5"
   },
-  {
-    "name": "ox",
-    "unicode": "1F402",
+  "ox": {
+    "category": "nature",
+    "moji": "🐂",
+    "unicodeVersion": "6.0",
     "digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed"
   },
-  {
-    "name": "package",
-    "unicode": "1F4E6",
+  "package": {
+    "category": "objects",
+    "moji": "📦",
+    "unicodeVersion": "6.0",
     "digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c"
   },
-  {
-    "name": "page_facing_up",
-    "unicode": "1F4C4",
+  "page_facing_up": {
+    "category": "objects",
+    "moji": "📄",
+    "unicodeVersion": "6.0",
     "digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a"
   },
-  {
-    "name": "page_with_curl",
-    "unicode": "1F4C3",
+  "page_with_curl": {
+    "category": "objects",
+    "moji": "📃",
+    "unicodeVersion": "6.0",
     "digest": "3d6257670189f841ad1fa45415c34feb2433b2cb35bb435c4ee122ce89b39669"
   },
-  {
-    "name": "pager",
-    "unicode": "1F4DF",
+  "pager": {
+    "category": "objects",
+    "moji": "📟",
+    "unicodeVersion": "6.0",
     "digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12"
   },
-  {
-    "name": "paintbrush",
-    "unicode": "1F58C",
-    "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
-  },
-  {
-    "name": "lower_left_paintbrush",
-    "unicode": "1F58C",
+  "paintbrush": {
+    "category": "objects",
+    "moji": "🖌",
+    "unicodeVersion": "7.0",
     "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
   },
-  {
-    "name": "palm_tree",
-    "unicode": "1F334",
+  "palm_tree": {
+    "category": "nature",
+    "moji": "🌴",
+    "unicodeVersion": "6.0",
     "digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1"
   },
-  {
-    "name": "pancakes",
-    "unicode": "1F95E",
+  "pancakes": {
+    "category": "food",
+    "moji": "🥞",
+    "unicodeVersion": "9.0",
     "digest": "5256b4832431e8a88555796b1a9726f12d909a26fb2bdc3a0abff76412c45903"
   },
-  {
-    "name": "panda_face",
-    "unicode": "1F43C",
+  "panda_face": {
+    "category": "nature",
+    "moji": "🐼",
+    "unicodeVersion": "6.0",
     "digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b"
   },
-  {
-    "name": "paperclip",
-    "unicode": "1F4CE",
+  "paperclip": {
+    "category": "objects",
+    "moji": "📎",
+    "unicodeVersion": "6.0",
     "digest": "d1e2ce94a12b7e8b7a9bba49e47ddc7432ec0288545d3b6817c7a499e806e3f0"
   },
-  {
-    "name": "paperclips",
-    "unicode": "1F587",
+  "paperclips": {
+    "category": "objects",
+    "moji": "🖇",
+    "unicodeVersion": "7.0",
     "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
   },
-  {
-    "name": "linked_paperclips",
-    "unicode": "1F587",
-    "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
-  },
-  {
-    "name": "park",
-    "unicode": "1F3DE",
-    "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
-  },
-  {
-    "name": "national_park",
-    "unicode": "1F3DE",
+  "park": {
+    "category": "travel",
+    "moji": "🏞",
+    "unicodeVersion": "7.0",
     "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
   },
-  {
-    "name": "parking",
-    "unicode": "1F17F",
+  "parking": {
+    "category": "symbols",
+    "moji": "🅿",
+    "unicodeVersion": "5.2",
     "digest": "9f1da460a7dd58b26beab8cf701be2691fb812208fbc941c71daa35be1507c2f"
   },
-  {
-    "name": "part_alternation_mark",
-    "unicode": "303D",
+  "part_alternation_mark": {
+    "category": "symbols",
+    "moji": "〽",
+    "unicodeVersion": "3.2",
     "digest": "956da19353bb38fd4dfe0ab5360679a9035d566858fb5de62887b85c75fb8eef"
   },
-  {
-    "name": "partly_sunny",
-    "unicode": "26C5",
+  "partly_sunny": {
+    "category": "nature",
+    "moji": "⛅",
+    "unicodeVersion": "5.2",
     "digest": "8fb9a6d2caf9e0cce58447762f0dfd6aa0b581b2e83fea6411348e0cbc8cf3c4"
   },
-  {
-    "name": "passport_control",
-    "unicode": "1F6C2",
+  "passport_control": {
+    "category": "symbols",
+    "moji": "🛂",
+    "unicodeVersion": "6.0",
     "digest": "d9be6eed2c90e1c89171c42d70a06485fdf86a4c68833371832cc1f6897fadd0"
   },
-  {
-    "name": "pause_button",
-    "unicode": "23F8",
+  "pause_button": {
+    "category": "symbols",
+    "moji": "⏸",
+    "unicodeVersion": "7.0",
     "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
   },
-  {
-    "name": "double_vertical_bar",
-    "unicode": "23F8",
-    "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
-  },
-  {
-    "name": "peace",
-    "unicode": "262E",
-    "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
-  },
-  {
-    "name": "peace_symbol",
-    "unicode": "262E",
+  "peace": {
+    "category": "symbols",
+    "moji": "☮",
+    "unicodeVersion": "1.1",
     "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
   },
-  {
-    "name": "peach",
-    "unicode": "1F351",
+  "peach": {
+    "category": "food",
+    "moji": "🍑",
+    "unicodeVersion": "6.0",
     "digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311"
   },
-  {
-    "name": "peanuts",
-    "unicode": "1F95C",
+  "peanuts": {
+    "category": "food",
+    "moji": "🥜",
+    "unicodeVersion": "9.0",
     "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
   },
-  {
-    "name": "shelled_peanut",
-    "unicode": "1F95C",
-    "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
-  },
-  {
-    "name": "pear",
-    "unicode": "1F350",
+  "pear": {
+    "category": "food",
+    "moji": "🍐",
+    "unicodeVersion": "6.0",
     "digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948"
   },
-  {
-    "name": "pen_ballpoint",
-    "unicode": "1F58A",
-    "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
-  },
-  {
-    "name": "lower_left_ballpoint_pen",
-    "unicode": "1F58A",
+  "pen_ballpoint": {
+    "category": "objects",
+    "moji": "🖊",
+    "unicodeVersion": "7.0",
     "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
   },
-  {
-    "name": "pen_fountain",
-    "unicode": "1F58B",
+  "pen_fountain": {
+    "category": "objects",
+    "moji": "🖋",
+    "unicodeVersion": "7.0",
     "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
   },
-  {
-    "name": "lower_left_fountain_pen",
-    "unicode": "1F58B",
-    "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
-  },
-  {
-    "name": "pencil",
-    "unicode": "1F4DD",
-    "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
-  },
-  {
-    "name": "memo",
-    "unicode": "1F4DD",
+  "pencil": {
+    "category": "objects",
+    "moji": "📝",
+    "unicodeVersion": "6.0",
     "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
   },
-  {
-    "name": "pencil2",
-    "unicode": "270F",
+  "pencil2": {
+    "category": "objects",
+    "moji": "✏",
+    "unicodeVersion": "1.1",
     "digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0"
   },
-  {
-    "name": "penguin",
-    "unicode": "1F427",
+  "penguin": {
+    "category": "nature",
+    "moji": "🐧",
+    "unicodeVersion": "6.0",
     "digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316"
   },
-  {
-    "name": "pensive",
-    "unicode": "1F614",
+  "pensive": {
+    "category": "people",
+    "moji": "😔",
+    "unicodeVersion": "6.0",
     "digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2"
   },
-  {
-    "name": "performing_arts",
-    "unicode": "1F3AD",
+  "performing_arts": {
+    "category": "activity",
+    "moji": "🎭",
+    "unicodeVersion": "6.0",
     "digest": "d7c7bc9213e308ca26286cbbd8012e656b0f9b00293758faf1bfccc4c5ceabed"
   },
-  {
-    "name": "persevere",
-    "unicode": "1F623",
+  "persevere": {
+    "category": "people",
+    "moji": "😣",
+    "unicodeVersion": "6.0",
     "digest": "c361509c9b8663af19a02a1ffff61b1b0d0b4bd75d693ce3d406b0ca1bde1ca0"
   },
-  {
-    "name": "person_frowning",
-    "unicode": "1F64D",
+  "person_frowning": {
+    "category": "people",
+    "moji": "🙍",
+    "unicodeVersion": "6.0",
     "digest": "b37be8bd95f21a6860ad3f171b8086125ab37331b382d87bcdb4cd684800546b"
   },
-  {
-    "name": "person_frowning_tone1",
-    "unicode": "1F64D-1F3FB",
+  "person_frowning_tone1": {
+    "category": "people",
+    "moji": "🙍🏻",
+    "unicodeVersion": "8.0",
     "digest": "3d5e78a367f9673baed2a86bc11cf04fd44394aadb65291fa51ade8dca318427"
   },
-  {
-    "name": "person_frowning_tone2",
-    "unicode": "1F64D-1F3FC",
+  "person_frowning_tone2": {
+    "category": "people",
+    "moji": "🙍🏼",
+    "unicodeVersion": "8.0",
     "digest": "7456c414c65ad6b6f11855f68a2eedc18113526f86862c4373202397cb1bed2c"
   },
-  {
-    "name": "person_frowning_tone3",
-    "unicode": "1F64D-1F3FD",
+  "person_frowning_tone3": {
+    "category": "people",
+    "moji": "🙍🏽",
+    "unicodeVersion": "8.0",
     "digest": "c86cf2d6951f1e6a7c786a74caaf68a777cf00e88023e23849d4383f864ae437"
   },
-  {
-    "name": "person_frowning_tone4",
-    "unicode": "1F64D-1F3FE",
+  "person_frowning_tone4": {
+    "category": "people",
+    "moji": "🙍🏾",
+    "unicodeVersion": "8.0",
     "digest": "944e96ced645ced8db6bb50120c7e37ed46b6960d595cbfe964c81803efa83aa"
   },
-  {
-    "name": "person_frowning_tone5",
-    "unicode": "1F64D-1F3FF",
+  "person_frowning_tone5": {
+    "category": "people",
+    "moji": "🙍🏿",
+    "unicodeVersion": "8.0",
     "digest": "4bd0ea571be6ef9f0493784ef0d12d5e47bc2d6ac610fb42c450bf3d87fb2948"
   },
-  {
-    "name": "person_with_blond_hair",
-    "unicode": "1F471",
+  "person_with_blond_hair": {
+    "category": "people",
+    "moji": "👱",
+    "unicodeVersion": "6.0",
     "digest": "a7f94ede2e43308108c2260d83fc10121dda09a67f94a0a840e6d7bba7fd5616"
   },
-  {
-    "name": "person_with_blond_hair_tone1",
-    "unicode": "1F471-1F3FB",
+  "person_with_blond_hair_tone1": {
+    "category": "people",
+    "moji": "👱🏻",
+    "unicodeVersion": "8.0",
     "digest": "00a116357a7878554c83e5bade4bddfa9cfabf76a229efa19cbb58e0d216219c"
   },
-  {
-    "name": "person_with_blond_hair_tone2",
-    "unicode": "1F471-1F3FC",
+  "person_with_blond_hair_tone2": {
+    "category": "people",
+    "moji": "👱🏼",
+    "unicodeVersion": "8.0",
     "digest": "df509ebe92ed3138b9d5bd4645eff4b13f77f714cf62bb949c59eff1adc00019"
   },
-  {
-    "name": "person_with_blond_hair_tone3",
-    "unicode": "1F471-1F3FD",
+  "person_with_blond_hair_tone3": {
+    "category": "people",
+    "moji": "👱🏽",
+    "unicodeVersion": "8.0",
     "digest": "6f328513f440a0c8cd1dc44596a5028fd8f306bdaf57c1e6f3aa94a3aa262b3c"
   },
-  {
-    "name": "person_with_blond_hair_tone4",
-    "unicode": "1F471-1F3FE",
+  "person_with_blond_hair_tone4": {
+    "category": "people",
+    "moji": "👱🏾",
+    "unicodeVersion": "8.0",
     "digest": "32df1a577815b009696643ad80d063cc97b35d54add6d4e5517fc936f6da9ee8"
   },
-  {
-    "name": "person_with_blond_hair_tone5",
-    "unicode": "1F471-1F3FF",
+  "person_with_blond_hair_tone5": {
+    "category": "people",
+    "moji": "👱🏿",
+    "unicodeVersion": "8.0",
     "digest": "2e270bb39187d8e36a33f4aa4d6045308189595fafc157cf7993e82d7ce93442"
   },
-  {
-    "name": "person_with_pouting_face",
-    "unicode": "1F64E",
+  "person_with_pouting_face": {
+    "category": "people",
+    "moji": "🙎",
+    "unicodeVersion": "6.0",
     "digest": "57e9a6e5f82121516dc189173f2a63b218f726cd51014e24a18c2bdfeeec3a0b"
   },
-  {
-    "name": "person_with_pouting_face_tone1",
-    "unicode": "1F64E-1F3FB",
+  "person_with_pouting_face_tone1": {
+    "category": "people",
+    "moji": "🙎🏻",
+    "unicodeVersion": "8.0",
     "digest": "d10dadb1ac03fc2e221eff77b4c47935dc0b4fe897af3de30461e7226c3b4bbc"
   },
-  {
-    "name": "person_with_pouting_face_tone2",
-    "unicode": "1F64E-1F3FC",
+  "person_with_pouting_face_tone2": {
+    "category": "people",
+    "moji": "🙎🏼",
+    "unicodeVersion": "8.0",
     "digest": "efface531537ab934b3b96985210a2dac88de812e82e804d6ec12174e536d1cc"
   },
-  {
-    "name": "person_with_pouting_face_tone3",
-    "unicode": "1F64E-1F3FD",
+  "person_with_pouting_face_tone3": {
+    "category": "people",
+    "moji": "🙎🏽",
+    "unicodeVersion": "8.0",
     "digest": "7ff26ece237216b949bfa96d16bd12cfd248c6fd3e4ed89aa6c735c09eafaeff"
   },
-  {
-    "name": "person_with_pouting_face_tone4",
-    "unicode": "1F64E-1F3FE",
+  "person_with_pouting_face_tone4": {
+    "category": "people",
+    "moji": "🙎🏾",
+    "unicodeVersion": "8.0",
     "digest": "045c04105df41d94ff4942133c7394e42ff35ef76c4ccb711497ab77ae6219f2"
   },
-  {
-    "name": "person_with_pouting_face_tone5",
-    "unicode": "1F64E-1F3FF",
+  "person_with_pouting_face_tone5": {
+    "category": "people",
+    "moji": "🙎🏿",
+    "unicodeVersion": "8.0",
     "digest": "783ee37f146fcf61d38af5009f5823cf6526fe99ed891979f454016bce9dd4ba"
   },
-  {
-    "name": "pick",
-    "unicode": "26CF",
+  "pick": {
+    "category": "objects",
+    "moji": "⛏",
+    "unicodeVersion": "5.2",
     "digest": "7f0ec5445b4d5c66cf46e2a7332946cce34bd70e9929ac7a119251a7f57f555d"
   },
-  {
-    "name": "pig",
-    "unicode": "1F437",
+  "pig": {
+    "category": "nature",
+    "moji": "🐷",
+    "unicodeVersion": "6.0",
     "digest": "51362570ab36805c8f67622ee4543e38811f8abb20f732a1af2ffbff2d63d042"
   },
-  {
-    "name": "pig2",
-    "unicode": "1F416",
+  "pig2": {
+    "category": "nature",
+    "moji": "🐖",
+    "unicodeVersion": "6.0",
     "digest": "67010e255f28061b9d9210bcdab6edc072642ad134122a1d0c7e3a6b1795a45b"
   },
-  {
-    "name": "pig_nose",
-    "unicode": "1F43D",
+  "pig_nose": {
+    "category": "nature",
+    "moji": "🐽",
+    "unicodeVersion": "6.0",
     "digest": "0b21cac238bf4910939fbea9bed35552378c1b605a3867d7b85c1556dbda22a9"
   },
-  {
-    "name": "pill",
-    "unicode": "1F48A",
+  "pill": {
+    "category": "objects",
+    "moji": "💊",
+    "unicodeVersion": "6.0",
     "digest": "cb00be361aaba6dbcf8da58bd20b76221dd75031362ecae99496b088ed413a7f"
   },
-  {
-    "name": "pineapple",
-    "unicode": "1F34D",
+  "pineapple": {
+    "category": "food",
+    "moji": "🍍",
+    "unicodeVersion": "6.0",
     "digest": "621d4d4c52b59e566c2e29ed7845c8bd2d1da0946577527342097808d170dd70"
   },
-  {
-    "name": "ping_pong",
-    "unicode": "1F3D3",
+  "ping_pong": {
+    "category": "activity",
+    "moji": "🏓",
+    "unicodeVersion": "8.0",
     "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
   },
-  {
-    "name": "table_tennis",
-    "unicode": "1F3D3",
-    "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
-  },
-  {
-    "name": "pisces",
-    "unicode": "2653",
+  "pisces": {
+    "category": "symbols",
+    "moji": "♓",
+    "unicodeVersion": "1.1",
     "digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a"
   },
-  {
-    "name": "pizza",
-    "unicode": "1F355",
+  "pizza": {
+    "category": "food",
+    "moji": "🍕",
+    "unicodeVersion": "6.0",
     "digest": "169bc6c1e1d7fdab1b8bf2eab0eeec4f9a7ae08b7b9b38f33b0b0c642e72053a"
   },
-  {
-    "name": "place_of_worship",
-    "unicode": "1F6D0",
-    "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
-  },
-  {
-    "name": "worship_symbol",
-    "unicode": "1F6D0",
+  "place_of_worship": {
+    "category": "symbols",
+    "moji": "🛐",
+    "unicodeVersion": "8.0",
     "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
   },
-  {
-    "name": "play_pause",
-    "unicode": "23EF",
+  "play_pause": {
+    "category": "symbols",
+    "moji": "⏯",
+    "unicodeVersion": "6.0",
     "digest": "af1498f34a3d6e0da8bbd26ebaa447e697e2df08c8eb255437cf7905c93f8c42"
   },
-  {
-    "name": "point_down",
-    "unicode": "1F447",
+  "point_down": {
+    "category": "people",
+    "moji": "👇",
+    "unicodeVersion": "6.0",
     "digest": "4ecdb3f31c16dc38113b8854ec1a7884613b688a185ebdf967eab9a81018f76d"
   },
-  {
-    "name": "point_down_tone1",
-    "unicode": "1F447-1F3FB",
+  "point_down_tone1": {
+    "category": "people",
+    "moji": "👇🏻",
+    "unicodeVersion": "8.0",
     "digest": "c74a7c94367cddbfa840542dc0924adeb0d108be0c7fde8c25fb95d69115d283"
   },
-  {
-    "name": "point_down_tone2",
-    "unicode": "1F447-1F3FC",
+  "point_down_tone2": {
+    "category": "people",
+    "moji": "👇🏼",
+    "unicodeVersion": "8.0",
     "digest": "dc4bda0726d85418b974addb42738f437fbb9cf16e5815cdbab3859c4ada6cae"
   },
-  {
-    "name": "point_down_tone3",
-    "unicode": "1F447-1F3FD",
+  "point_down_tone3": {
+    "category": "people",
+    "moji": "👇🏽",
+    "unicodeVersion": "8.0",
     "digest": "e460f81a501376d2f0ed1d45e358c5ed03ba049e8f466e4298afb4f3ca6d24dc"
   },
-  {
-    "name": "point_down_tone4",
-    "unicode": "1F447-1F3FE",
+  "point_down_tone4": {
+    "category": "people",
+    "moji": "👇🏾",
+    "unicodeVersion": "8.0",
     "digest": "4bc91cd771f24e0f897a9d8b18f323fec9a82da0fc2429c4a7e4e6a9d885a0a3"
   },
-  {
-    "name": "point_down_tone5",
-    "unicode": "1F447-1F3FF",
+  "point_down_tone5": {
+    "category": "people",
+    "moji": "👇🏿",
+    "unicodeVersion": "8.0",
     "digest": "7e47c6bc73250f36dc7ae1c1c09e7b41f30647b9d0ff703a53a75cc046b5057d"
   },
-  {
-    "name": "point_left",
-    "unicode": "1F448",
+  "point_left": {
+    "category": "people",
+    "moji": "👈",
+    "unicodeVersion": "6.0",
     "digest": "b5a7e864a0016afbadb3bec41f51ecf8c4af73cc20462e1a08b357f90bca6879"
   },
-  {
-    "name": "point_left_tone1",
-    "unicode": "1F448-1F3FB",
+  "point_left_tone1": {
+    "category": "people",
+    "moji": "👈🏻",
+    "unicodeVersion": "8.0",
     "digest": "9f1868272a10a2b738c065be5d30241643324550cfd47baf01c7a09060e66d31"
   },
-  {
-    "name": "point_left_tone2",
-    "unicode": "1F448-1F3FC",
+  "point_left_tone2": {
+    "category": "people",
+    "moji": "👈🏼",
+    "unicodeVersion": "8.0",
     "digest": "bf0d58c68178a2c2c01d4a6235a1a66b90073cea170f9f6fe2668b6dd68424f7"
   },
-  {
-    "name": "point_left_tone3",
-    "unicode": "1F448-1F3FD",
+  "point_left_tone3": {
+    "category": "people",
+    "moji": "👈🏽",
+    "unicodeVersion": "8.0",
     "digest": "34d28c97bc8f9d111d14e328153c4298fc32cf18e39e20aacaec17846645ed90"
   },
-  {
-    "name": "point_left_tone4",
-    "unicode": "1F448-1F3FE",
+  "point_left_tone4": {
+    "category": "people",
+    "moji": "👈🏾",
+    "unicodeVersion": "8.0",
     "digest": "c40c8436316915d516c53bb1c98a469528cefd98baa719be7e748c4608cbbcc9"
   },
-  {
-    "name": "point_left_tone5",
-    "unicode": "1F448-1F3FF",
+  "point_left_tone5": {
+    "category": "people",
+    "moji": "👈🏿",
+    "unicodeVersion": "8.0",
     "digest": "c410fe32e4ce0ded74845a54b86090e59e5820d457837b16e175b36cc71ecb46"
   },
-  {
-    "name": "point_right",
-    "unicode": "1F449",
+  "point_right": {
+    "category": "people",
+    "moji": "👉",
+    "unicodeVersion": "6.0",
     "digest": "44d9251ab41f2f48c2250c44a47f92b3476a71f13fbbbfb637547db837fd5a49"
   },
-  {
-    "name": "point_right_tone1",
-    "unicode": "1F449-1F3FB",
+  "point_right_tone1": {
+    "category": "people",
+    "moji": "👉🏻",
+    "unicodeVersion": "8.0",
     "digest": "9fcce259eb81c0b52ec7796b98a1653194e3a9021a1d338df1dbbab7522fc406"
   },
-  {
-    "name": "point_right_tone2",
-    "unicode": "1F449-1F3FC",
+  "point_right_tone2": {
+    "category": "people",
+    "moji": "👉🏼",
+    "unicodeVersion": "8.0",
     "digest": "9d00a0b1cfc435674dc56065b3d28d28839196977504cf20581205351d8708f2"
   },
-  {
-    "name": "point_right_tone3",
-    "unicode": "1F449-1F3FD",
+  "point_right_tone3": {
+    "category": "people",
+    "moji": "👉🏽",
+    "unicodeVersion": "8.0",
     "digest": "e3026a70630ba73d76892a055a80cac2f78d509faddce737f802d2abefa074ba"
   },
-  {
-    "name": "point_right_tone4",
-    "unicode": "1F449-1F3FE",
+  "point_right_tone4": {
+    "category": "people",
+    "moji": "👉🏾",
+    "unicodeVersion": "8.0",
     "digest": "ea508fde90561460361773b4e1b8e80874667b19ac115926206e7c592587cb76"
   },
-  {
-    "name": "point_right_tone5",
-    "unicode": "1F449-1F3FF",
+  "point_right_tone5": {
+    "category": "people",
+    "moji": "👉🏿",
+    "unicodeVersion": "8.0",
     "digest": "d59cdb2864eb2929941ecd233f8b8afcddc30fbd4594e5f9acf6386ae06ac12c"
   },
-  {
-    "name": "point_up",
-    "unicode": "261D",
+  "point_up": {
+    "category": "people",
+    "moji": "☝",
+    "unicodeVersion": "1.1",
     "digest": "b69ff4f650989709f2185822d278c7773672bd9eb4a625da80f3038a2b9ce42b"
   },
-  {
-    "name": "point_up_2",
-    "unicode": "1F446",
+  "point_up_2": {
+    "category": "people",
+    "moji": "👆",
+    "unicodeVersion": "6.0",
     "digest": "e83cd9eff2af5125a25f5a306c3ee3cfea240add683b5c36a86a994a8d8c805c"
   },
-  {
-    "name": "point_up_2_tone1",
-    "unicode": "1F446-1F3FB",
+  "point_up_2_tone1": {
+    "category": "people",
+    "moji": "👆🏻",
+    "unicodeVersion": "8.0",
     "digest": "b02ec3e7e04a83bfb769cffb951cbf32aa78e56fa5a51c097f9326df9e08ed33"
   },
-  {
-    "name": "point_up_2_tone2",
-    "unicode": "1F446-1F3FC",
+  "point_up_2_tone2": {
+    "category": "people",
+    "moji": "👆🏼",
+    "unicodeVersion": "8.0",
     "digest": "32994b85c8b4a1383ca985ebc3382be88866cea1ff1315adfb71fb05e992a232"
   },
-  {
-    "name": "point_up_2_tone3",
-    "unicode": "1F446-1F3FD",
+  "point_up_2_tone3": {
+    "category": "people",
+    "moji": "👆🏽",
+    "unicodeVersion": "8.0",
     "digest": "9e263bcfb82ada34ff85291f36e64e66b86760fb11a4e0c554e801644d417d6d"
   },
-  {
-    "name": "point_up_2_tone4",
-    "unicode": "1F446-1F3FE",
+  "point_up_2_tone4": {
+    "category": "people",
+    "moji": "👆🏾",
+    "unicodeVersion": "8.0",
     "digest": "3edc92130a0851ac7b5236772ce7918d088689221df287098688e1ed5b3ff181"
   },
-  {
-    "name": "point_up_2_tone5",
-    "unicode": "1F446-1F3FF",
+  "point_up_2_tone5": {
+    "category": "people",
+    "moji": "👆🏿",
+    "unicodeVersion": "8.0",
     "digest": "cabb3b7da9290840ef59d0c8b22625bdb2e94842f01b0a575ccbc348f3069d77"
   },
-  {
-    "name": "point_up_tone1",
-    "unicode": "261D-1F3FB",
+  "point_up_tone1": {
+    "category": "people",
+    "moji": "☝🏻",
+    "unicodeVersion": "8.0",
     "digest": "e496fda349072f8b321ceb7a251175f7244c3076661f5ede48ea75ba1acf8339"
   },
-  {
-    "name": "point_up_tone2",
-    "unicode": "261D-1F3FC",
+  "point_up_tone2": {
+    "category": "people",
+    "moji": "☝🏼",
+    "unicodeVersion": "8.0",
     "digest": "5a8081323f3baa67e6431e21e16a36559b339f5175d586644e34947f738dd07a"
   },
-  {
-    "name": "point_up_tone3",
-    "unicode": "261D-1F3FD",
+  "point_up_tone3": {
+    "category": "people",
+    "moji": "☝🏽",
+    "unicodeVersion": "8.0",
     "digest": "07bf0cea812eb226b443334e026e13d1ec23e013478f4af862a3919703107842"
   },
-  {
-    "name": "point_up_tone4",
-    "unicode": "261D-1F3FE",
+  "point_up_tone4": {
+    "category": "people",
+    "moji": "☝🏾",
+    "unicodeVersion": "8.0",
     "digest": "1fbbd71433108143ee157d0fdadd183f7f013bafa96f0dd93b181e1fd5fd4af2"
   },
-  {
-    "name": "point_up_tone5",
-    "unicode": "261D-1F3FF",
+  "point_up_tone5": {
+    "category": "people",
+    "moji": "☝🏿",
+    "unicodeVersion": "8.0",
     "digest": "ad068ef32df32f8297955490a9a90590a0f93ed5702a052cd0d8f6484c6cc679"
   },
-  {
-    "name": "police_car",
-    "unicode": "1F693",
+  "police_car": {
+    "category": "travel",
+    "moji": "🚓",
+    "unicodeVersion": "6.0",
     "digest": "0909be1bd615ae331a7cce71e16dee3ca663c721d5170072c593cb7c76f9f661"
   },
-  {
-    "name": "poodle",
-    "unicode": "1F429",
+  "poodle": {
+    "category": "nature",
+    "moji": "🐩",
+    "unicodeVersion": "6.0",
     "digest": "f1742fdf3fd26a8a5cfeaba57026518dacaad364cbd03344c4000a35af13e47a"
   },
-  {
-    "name": "poop",
-    "unicode": "1F4A9",
+  "poop": {
+    "category": "people",
+    "moji": "💩",
+    "unicodeVersion": "6.0",
     "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
   },
-  {
-    "name": "shit",
-    "unicode": "1F4A9",
-    "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
-  },
-  {
-    "name": "hankey",
-    "unicode": "1F4A9",
-    "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
-  },
-  {
-    "name": "poo",
-    "unicode": "1F4A9",
-    "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
-  },
-  {
-    "name": "popcorn",
-    "unicode": "1F37F",
+  "popcorn": {
+    "category": "food",
+    "moji": "🍿",
+    "unicodeVersion": "8.0",
     "digest": "684f1b7ef34ea7ca933aed41569bc6595a19ef0d546a1b7b9e69f8335540b323"
   },
-  {
-    "name": "post_office",
-    "unicode": "1F3E3",
+  "post_office": {
+    "category": "travel",
+    "moji": "🏣",
+    "unicodeVersion": "6.0",
     "digest": "54398ee396c1314a7993b1cb1cba264946b5c9d5a7dbb43fd67286854d1d1a0f"
   },
-  {
-    "name": "postal_horn",
-    "unicode": "1F4EF",
+  "postal_horn": {
+    "category": "objects",
+    "moji": "📯",
+    "unicodeVersion": "6.0",
     "digest": "0ea12f44f3bae9a14bde3b37361b48bd738d2f613bb1b53a9204959b70e643f8"
   },
-  {
-    "name": "postbox",
-    "unicode": "1F4EE",
+  "postbox": {
+    "category": "objects",
+    "moji": "📮",
+    "unicodeVersion": "6.0",
     "digest": "bbc424ae8d46de380d7023a43ea064002fd614657d00330d3503275827ac87e2"
   },
-  {
-    "name": "potable_water",
-    "unicode": "1F6B0",
+  "potable_water": {
+    "category": "symbols",
+    "moji": "🚰",
+    "unicodeVersion": "6.0",
     "digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098"
   },
-  {
-    "name": "potato",
-    "unicode": "1F954",
+  "potato": {
+    "category": "food",
+    "moji": "🥔",
+    "unicodeVersion": "9.0",
     "digest": "a56a69f36f3a0793f278726d92c0cea2960554f3062ef1a0904526a04511d8e1"
   },
-  {
-    "name": "pouch",
-    "unicode": "1F45D",
+  "pouch": {
+    "category": "people",
+    "moji": "👝",
+    "unicodeVersion": "6.0",
     "digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351"
   },
-  {
-    "name": "poultry_leg",
-    "unicode": "1F357",
+  "poultry_leg": {
+    "category": "food",
+    "moji": "🍗",
+    "unicodeVersion": "6.0",
     "digest": "1445ec4f5e68a19e5a84e5537dca8190d62409070c954d112e6097f1a6b7f054"
   },
-  {
-    "name": "pound",
-    "unicode": "1F4B7",
+  "pound": {
+    "category": "objects",
+    "moji": "💷",
+    "unicodeVersion": "6.0",
     "digest": "eb11b83eb52adb0a15e69a3bc15788a2dc7825dedee81ac3af84963c9dd517b5"
   },
-  {
-    "name": "pouting_cat",
-    "unicode": "1F63E",
+  "pouting_cat": {
+    "category": "people",
+    "moji": "😾",
+    "unicodeVersion": "6.0",
     "digest": "8822abedf3499cf98278d7eeea0764d1100ec25cad71b4b2e877f9346f8c8138"
   },
-  {
-    "name": "pray",
-    "unicode": "1F64F",
+  "pray": {
+    "category": "people",
+    "moji": "🙏",
+    "unicodeVersion": "6.0",
     "digest": "735b79dab34ac2cf81fd42fdcd7eb1f13c24655e5e343816d5764896c03edeea"
   },
-  {
-    "name": "pray_tone1",
-    "unicode": "1F64F-1F3FB",
+  "pray_tone1": {
+    "category": "people",
+    "moji": "🙏🏻",
+    "unicodeVersion": "8.0",
     "digest": "e8b6103450215e8566797f150978355e297deade4eb47a6371f7a7bc558fed9d"
   },
-  {
-    "name": "pray_tone2",
-    "unicode": "1F64F-1F3FC",
+  "pray_tone2": {
+    "category": "people",
+    "moji": "🙏🏼",
+    "unicodeVersion": "8.0",
     "digest": "ee8baacd95d7e8dbad8a1f2d9a12e36c98f3d518db5d3b117d0a18290815e62b"
   },
-  {
-    "name": "pray_tone3",
-    "unicode": "1F64F-1F3FD",
+  "pray_tone3": {
+    "category": "people",
+    "moji": "🙏🏽",
+    "unicodeVersion": "8.0",
     "digest": "ae8c0caa9aca0a6c44069e76a7535c961d0284cd701812f76bbd2bd79ce2bd53"
   },
-  {
-    "name": "pray_tone4",
-    "unicode": "1F64F-1F3FE",
+  "pray_tone4": {
+    "category": "people",
+    "moji": "🙏🏾",
+    "unicodeVersion": "8.0",
     "digest": "64f7b3178b8cd6f6a877ed583539eefe068fa87a0dd658fdcd58c8bc809f7e17"
   },
-  {
-    "name": "pray_tone5",
-    "unicode": "1F64F-1F3FF",
+  "pray_tone5": {
+    "category": "people",
+    "moji": "🙏🏿",
+    "unicodeVersion": "8.0",
     "digest": "5bc8cdce937ac06779c87021423efcec4f602aa4a39dba90b00de81033005332"
   },
-  {
-    "name": "prayer_beads",
-    "unicode": "1F4FF",
+  "prayer_beads": {
+    "category": "objects",
+    "moji": "📿",
+    "unicodeVersion": "8.0",
     "digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05"
   },
-  {
-    "name": "pregnant_woman",
-    "unicode": "1F930",
-    "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
-  },
-  {
-    "name": "expecting_woman",
-    "unicode": "1F930",
+  "pregnant_woman": {
+    "category": "people",
+    "moji": "🤰",
+    "unicodeVersion": "9.0",
     "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
   },
-  {
-    "name": "pregnant_woman_tone1",
-    "unicode": "1F930-1F3FB",
+  "pregnant_woman_tone1": {
+    "category": "people",
+    "moji": "🤰🏻",
+    "unicodeVersion": "9.0",
     "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
   },
-  {
-    "name": "expecting_woman_tone1",
-    "unicode": "1F930-1F3FB",
-    "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
-  },
-  {
-    "name": "pregnant_woman_tone2",
-    "unicode": "1F930-1F3FC",
-    "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
-  },
-  {
-    "name": "expecting_woman_tone2",
-    "unicode": "1F930-1F3FC",
+  "pregnant_woman_tone2": {
+    "category": "people",
+    "moji": "🤰🏼",
+    "unicodeVersion": "9.0",
     "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
   },
-  {
-    "name": "pregnant_woman_tone3",
-    "unicode": "1F930-1F3FD",
+  "pregnant_woman_tone3": {
+    "category": "people",
+    "moji": "🤰🏽",
+    "unicodeVersion": "9.0",
     "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
   },
-  {
-    "name": "expecting_woman_tone3",
-    "unicode": "1F930-1F3FD",
-    "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
-  },
-  {
-    "name": "pregnant_woman_tone4",
-    "unicode": "1F930-1F3FE",
-    "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
-  },
-  {
-    "name": "expecting_woman_tone4",
-    "unicode": "1F930-1F3FE",
+  "pregnant_woman_tone4": {
+    "category": "people",
+    "moji": "🤰🏾",
+    "unicodeVersion": "9.0",
     "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
   },
-  {
-    "name": "pregnant_woman_tone5",
-    "unicode": "1F930-1F3FF",
+  "pregnant_woman_tone5": {
+    "category": "people",
+    "moji": "🤰🏿",
+    "unicodeVersion": "9.0",
     "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
   },
-  {
-    "name": "expecting_woman_tone5",
-    "unicode": "1F930-1F3FF",
-    "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
-  },
-  {
-    "name": "prince",
-    "unicode": "1F934",
+  "prince": {
+    "category": "people",
+    "moji": "🤴",
+    "unicodeVersion": "9.0",
     "digest": "34a0e0625f0a9825d3674192d6233b6cae4d8130451293df09f91a6a4165869c"
   },
-  {
-    "name": "prince_tone1",
-    "unicode": "1F934-1F3FB",
+  "prince_tone1": {
+    "category": "people",
+    "moji": "🤴🏻",
+    "unicodeVersion": "9.0",
     "digest": "ccecdfeccb2ab1fceceae14f3fba875c8c7099785a4c40131c08a697b5b675fc"
   },
-  {
-    "name": "prince_tone2",
-    "unicode": "1F934-1F3FC",
+  "prince_tone2": {
+    "category": "people",
+    "moji": "🤴🏼",
+    "unicodeVersion": "9.0",
     "digest": "c373fd3e0c1798415e3d8d88fab6c98c1bbdedcbe6f52f3a3899f6e2124a768d"
   },
-  {
-    "name": "prince_tone3",
-    "unicode": "1F934-1F3FD",
+  "prince_tone3": {
+    "category": "people",
+    "moji": "🤴🏽",
+    "unicodeVersion": "9.0",
     "digest": "71d15695ca954d55aa69d3c753c7d31a8ba5329713a8ddbc90dafc11e524c4ef"
   },
-  {
-    "name": "prince_tone4",
-    "unicode": "1F934-1F3FE",
+  "prince_tone4": {
+    "category": "people",
+    "moji": "🤴🏾",
+    "unicodeVersion": "9.0",
     "digest": "08f6cb32424f15cc3aaf83c31a5dac7c01a6be2f37ea8f13aed579ce6fb4db19"
   },
-  {
-    "name": "prince_tone5",
-    "unicode": "1F934-1F3FF",
+  "prince_tone5": {
+    "category": "people",
+    "moji": "🤴🏿",
+    "unicodeVersion": "9.0",
     "digest": "77d521148efa33fa4d3409693d050fecfd948411e807327484f174e289834649"
   },
-  {
-    "name": "princess",
-    "unicode": "1F478",
+  "princess": {
+    "category": "people",
+    "moji": "👸",
+    "unicodeVersion": "6.0",
     "digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80"
   },
-  {
-    "name": "princess_tone1",
-    "unicode": "1F478-1F3FB",
+  "princess_tone1": {
+    "category": "people",
+    "moji": "👸🏻",
+    "unicodeVersion": "8.0",
     "digest": "52b88b99ba64f82e8f36e2a1827c85145e4fcd6863478c2345fe9fa9e8901cdf"
   },
-  {
-    "name": "princess_tone2",
-    "unicode": "1F478-1F3FC",
+  "princess_tone2": {
+    "category": "people",
+    "moji": "👸🏼",
+    "unicodeVersion": "8.0",
     "digest": "7e44289404693668f20e681fcdc2e516512d54a69c627eedae958f69dfe6eea9"
   },
-  {
-    "name": "princess_tone3",
-    "unicode": "1F478-1F3FD",
+  "princess_tone3": {
+    "category": "people",
+    "moji": "👸🏽",
+    "unicodeVersion": "8.0",
     "digest": "96c9a9857348d7a1a8be899c50d55b352b9a9fd5c65e4777bfa199fe7929d41c"
   },
-  {
-    "name": "princess_tone4",
-    "unicode": "1F478-1F3FE",
+  "princess_tone4": {
+    "category": "people",
+    "moji": "👸🏾",
+    "unicodeVersion": "8.0",
     "digest": "67696f96be60f2a36598072172d2db197d007e6c1ac3acef526a5ce6d59bf3f7"
   },
-  {
-    "name": "princess_tone5",
-    "unicode": "1F478-1F3FF",
+  "princess_tone5": {
+    "category": "people",
+    "moji": "👸🏿",
+    "unicodeVersion": "8.0",
     "digest": "007f624e2fad91bb57ce32ecd35213a796d71807f3b12f3f1575bf50e6a50eeb"
   },
-  {
-    "name": "printer",
-    "unicode": "1F5A8",
+  "printer": {
+    "category": "objects",
+    "moji": "🖨",
+    "unicodeVersion": "7.0",
     "digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8"
   },
-  {
-    "name": "projector",
-    "unicode": "1F4FD",
-    "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
-  },
-  {
-    "name": "film_projector",
-    "unicode": "1F4FD",
+  "projector": {
+    "category": "objects",
+    "moji": "📽",
+    "unicodeVersion": "7.0",
     "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
   },
-  {
-    "name": "punch",
-    "unicode": "1F44A",
+  "punch": {
+    "category": "people",
+    "moji": "👊",
+    "unicodeVersion": "6.0",
     "digest": "c7e7edf6d64f755db3f02874354f08337b3971aff329476d19ac946e0b421329"
   },
-  {
-    "name": "punch_tone1",
-    "unicode": "1F44A-1F3FB",
+  "punch_tone1": {
+    "category": "people",
+    "moji": "👊🏻",
+    "unicodeVersion": "8.0",
     "digest": "c9ba508b0c36041047473782acfedab5af40dd7946b33daf4d8d54c726e06a11"
   },
-  {
-    "name": "punch_tone2",
-    "unicode": "1F44A-1F3FC",
+  "punch_tone2": {
+    "category": "people",
+    "moji": "👊🏼",
+    "unicodeVersion": "8.0",
     "digest": "d53011cd2f3334c7b3fffdfe1e2b8cc1c832c74306e1ac6d03f954a1309d7d0b"
   },
-  {
-    "name": "punch_tone3",
-    "unicode": "1F44A-1F3FD",
+  "punch_tone3": {
+    "category": "people",
+    "moji": "👊🏽",
+    "unicodeVersion": "8.0",
     "digest": "f7522347094e0130ed8e304678106574dbd7dd2b6b3aeb4d8a7a0fef880920b2"
   },
-  {
-    "name": "punch_tone4",
-    "unicode": "1F44A-1F3FE",
+  "punch_tone4": {
+    "category": "people",
+    "moji": "👊🏾",
+    "unicodeVersion": "8.0",
     "digest": "3e62bdd426f3e6ff175ce3b8dd6f6d3998d9c1506128defa96b528b455295b47"
   },
-  {
-    "name": "punch_tone5",
-    "unicode": "1F44A-1F3FF",
+  "punch_tone5": {
+    "category": "people",
+    "moji": "👊🏿",
+    "unicodeVersion": "8.0",
     "digest": "7d9bff777dc4ec41ac132b1252fa08cf92a398c8dc146c4a5327b45d568982d8"
   },
-  {
-    "name": "purple_heart",
-    "unicode": "1F49C",
+  "purple_heart": {
+    "category": "symbols",
+    "moji": "💜",
+    "unicodeVersion": "6.0",
     "digest": "a6bf01de806525942be480e45a4b2879f91df8129b78a1b8734d4f917bcab773"
   },
-  {
-    "name": "purse",
-    "unicode": "1F45B",
+  "purse": {
+    "category": "people",
+    "moji": "👛",
+    "unicodeVersion": "6.0",
     "digest": "2b785f36e01875d66cfda2192c8c53606e7224a7c869a4826b62cb61613d60c8"
   },
-  {
-    "name": "pushpin",
-    "unicode": "1F4CC",
+  "pushpin": {
+    "category": "objects",
+    "moji": "📌",
+    "unicodeVersion": "6.0",
     "digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8"
   },
-  {
-    "name": "put_litter_in_its_place",
-    "unicode": "1F6AE",
+  "put_litter_in_its_place": {
+    "category": "symbols",
+    "moji": "🚮",
+    "unicodeVersion": "6.0",
     "digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c"
   },
-  {
-    "name": "question",
-    "unicode": "2753",
+  "question": {
+    "category": "symbols",
+    "moji": "❓",
+    "unicodeVersion": "6.0",
     "digest": "40050a1fd29bed321fd601d13dc33de5d6084121f1d873b29bde9dc3d823a310"
   },
-  {
-    "name": "rabbit",
-    "unicode": "1F430",
+  "rabbit": {
+    "category": "nature",
+    "moji": "🐰",
+    "unicodeVersion": "6.0",
     "digest": "678ad953a7ab8f618c59051449a67c965d1f04f42dd6f6669adaf3fadebd080c"
   },
-  {
-    "name": "rabbit2",
-    "unicode": "1F407",
+  "rabbit2": {
+    "category": "nature",
+    "moji": "🐇",
+    "unicodeVersion": "6.0",
     "digest": "19b1f5108292472434cc7a49efac4ea9275779735c7aeb0f15c36021d5998ca0"
   },
-  {
-    "name": "race_car",
-    "unicode": "1F3CE",
+  "race_car": {
+    "category": "travel",
+    "moji": "🏎",
+    "unicodeVersion": "7.0",
     "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
   },
-  {
-    "name": "racing_car",
-    "unicode": "1F3CE",
-    "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
-  },
-  {
-    "name": "racehorse",
-    "unicode": "1F40E",
+  "racehorse": {
+    "category": "nature",
+    "moji": "🐎",
+    "unicodeVersion": "6.0",
     "digest": "a57b7aca35347ada8225eeee06b70cfd040484104963b4df56ea8fec690576b0"
   },
-  {
-    "name": "radio",
-    "unicode": "1F4FB",
+  "radio": {
+    "category": "objects",
+    "moji": "📻",
+    "unicodeVersion": "6.0",
     "digest": "9245951dd779cdd141089891b15a90d3999a6358acf1fc296aa505100f812108"
   },
-  {
-    "name": "radio_button",
-    "unicode": "1F518",
+  "radio_button": {
+    "category": "symbols",
+    "moji": "🔘",
+    "unicodeVersion": "6.0",
     "digest": "565bec59198df2592e96564c6e314d3cde33c47b453db1bec6c5d027b5cb4fd9"
   },
-  {
-    "name": "radioactive",
-    "unicode": "2622",
-    "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
-  },
-  {
-    "name": "radioactive_sign",
-    "unicode": "2622",
+  "radioactive": {
+    "category": "symbols",
+    "moji": "☢",
+    "unicodeVersion": "1.1",
     "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
   },
-  {
-    "name": "rage",
-    "unicode": "1F621",
+  "rage": {
+    "category": "people",
+    "moji": "😡",
+    "unicodeVersion": "6.0",
     "digest": "d97ba6bd08eec46dbc7199f530c945b73a87a878e35397b0a3e4f2b45039e89e"
   },
-  {
-    "name": "railway_car",
-    "unicode": "1F683",
+  "railway_car": {
+    "category": "travel",
+    "moji": "🚃",
+    "unicodeVersion": "6.0",
     "digest": "2cddc08d555e7fc24e312c3d255ed013fbf9cd2974a6918369c32554049ba2be"
   },
-  {
-    "name": "railway_track",
-    "unicode": "1F6E4",
+  "railway_track": {
+    "category": "travel",
+    "moji": "🛤",
+    "unicodeVersion": "7.0",
     "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
   },
-  {
-    "name": "railroad_track",
-    "unicode": "1F6E4",
-    "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
-  },
-  {
-    "name": "rainbow",
-    "unicode": "1F308",
+  "rainbow": {
+    "category": "travel",
+    "moji": "🌈",
+    "unicodeVersion": "6.0",
     "digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d"
   },
-  {
-    "name": "raised_back_of_hand",
-    "unicode": "1F91A",
-    "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
-  },
-  {
-    "name": "back_of_hand",
-    "unicode": "1F91A",
+  "raised_back_of_hand": {
+    "category": "people",
+    "moji": "🤚",
+    "unicodeVersion": "9.0",
     "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
   },
-  {
-    "name": "raised_back_of_hand_tone1",
-    "unicode": "1F91A-1F3FB",
+  "raised_back_of_hand_tone1": {
+    "category": "people",
+    "moji": "🤚🏻",
+    "unicodeVersion": "9.0",
     "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
   },
-  {
-    "name": "back_of_hand_tone1",
-    "unicode": "1F91A-1F3FB",
-    "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
-  },
-  {
-    "name": "raised_back_of_hand_tone2",
-    "unicode": "1F91A-1F3FC",
-    "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
-  },
-  {
-    "name": "back_of_hand_tone2",
-    "unicode": "1F91A-1F3FC",
+  "raised_back_of_hand_tone2": {
+    "category": "people",
+    "moji": "🤚🏼",
+    "unicodeVersion": "9.0",
     "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
   },
-  {
-    "name": "raised_back_of_hand_tone3",
-    "unicode": "1F91A-1F3FD",
+  "raised_back_of_hand_tone3": {
+    "category": "people",
+    "moji": "🤚🏽",
+    "unicodeVersion": "9.0",
     "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
   },
-  {
-    "name": "back_of_hand_tone3",
-    "unicode": "1F91A-1F3FD",
-    "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
-  },
-  {
-    "name": "raised_back_of_hand_tone4",
-    "unicode": "1F91A-1F3FE",
-    "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
-  },
-  {
-    "name": "back_of_hand_tone4",
-    "unicode": "1F91A-1F3FE",
+  "raised_back_of_hand_tone4": {
+    "category": "people",
+    "moji": "🤚🏾",
+    "unicodeVersion": "9.0",
     "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
   },
-  {
-    "name": "raised_back_of_hand_tone5",
-    "unicode": "1F91A-1F3FF",
+  "raised_back_of_hand_tone5": {
+    "category": "people",
+    "moji": "🤚🏿",
+    "unicodeVersion": "9.0",
     "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
   },
-  {
-    "name": "back_of_hand_tone5",
-    "unicode": "1F91A-1F3FF",
-    "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
-  },
-  {
-    "name": "raised_hand",
-    "unicode": "270B",
+  "raised_hand": {
+    "category": "people",
+    "moji": "✋",
+    "unicodeVersion": "6.0",
     "digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a"
   },
-  {
-    "name": "raised_hand_tone1",
-    "unicode": "270B-1F3FB",
+  "raised_hand_tone1": {
+    "category": "people",
+    "moji": "✋🏻",
+    "unicodeVersion": "8.0",
     "digest": "865afca29b57577fed8fe8c2be57b74254a008c8cf34194680be2759239b5f5d"
   },
-  {
-    "name": "raised_hand_tone2",
-    "unicode": "270B-1F3FC",
+  "raised_hand_tone2": {
+    "category": "people",
+    "moji": "✋🏼",
+    "unicodeVersion": "8.0",
     "digest": "832169a0b626a682a58a3b998f68413657b4962c1fab05f1fdc2668e82727210"
   },
-  {
-    "name": "raised_hand_tone3",
-    "unicode": "270B-1F3FD",
+  "raised_hand_tone3": {
+    "category": "people",
+    "moji": "✋🏽",
+    "unicodeVersion": "8.0",
     "digest": "3959a873ad7671de82c615c4ed840b011e67baafb2bab7dd16859608d3e83cb1"
   },
-  {
-    "name": "raised_hand_tone4",
-    "unicode": "270B-1F3FE",
+  "raised_hand_tone4": {
+    "category": "people",
+    "moji": "✋🏾",
+    "unicodeVersion": "8.0",
     "digest": "db542f65d076ccf3dbfca27cb7c2f135a8bf7a487a81a04873e70172bdfcd579"
   },
-  {
-    "name": "raised_hand_tone5",
-    "unicode": "270B-1F3FF",
+  "raised_hand_tone5": {
+    "category": "people",
+    "moji": "✋🏿",
+    "unicodeVersion": "8.0",
     "digest": "88ca884d14baaae48df21d75c22d82fb15bdc395e42026f5ca34cd65e5ae8674"
   },
-  {
-    "name": "raised_hands",
-    "unicode": "1F64C",
+  "raised_hands": {
+    "category": "people",
+    "moji": "🙌",
+    "unicodeVersion": "6.0",
     "digest": "2ee73466a3f5079e542857fe6f5497e9f87753a81854985ce3356a8d3da1d8b8"
   },
-  {
-    "name": "raised_hands_tone1",
-    "unicode": "1F64C-1F3FB",
+  "raised_hands_tone1": {
+    "category": "people",
+    "moji": "🙌🏻",
+    "unicodeVersion": "8.0",
     "digest": "43e73c60f040a66374b8ec98f3629a90d13ae9f472446ed7676cd5573e824f4b"
   },
-  {
-    "name": "raised_hands_tone2",
-    "unicode": "1F64C-1F3FC",
+  "raised_hands_tone2": {
+    "category": "people",
+    "moji": "🙌🏼",
+    "unicodeVersion": "8.0",
     "digest": "fcc5255bb2b06dc82d6878e74cf34e8ce118c70004a06d39a980683772b98c52"
   },
-  {
-    "name": "raised_hands_tone3",
-    "unicode": "1F64C-1F3FD",
+  "raised_hands_tone3": {
+    "category": "people",
+    "moji": "🙌🏽",
+    "unicodeVersion": "8.0",
     "digest": "3ee3e0aafef486e766a166935e8147fb75a7329cfebc96dec876cc45e83a8754"
   },
-  {
-    "name": "raised_hands_tone4",
-    "unicode": "1F64C-1F3FE",
+  "raised_hands_tone4": {
+    "category": "people",
+    "moji": "🙌🏾",
+    "unicodeVersion": "8.0",
     "digest": "78a8cbf6b2b85be4d6b18f0ff6a77f197963117955725fb7e57e0441effb928f"
   },
-  {
-    "name": "raised_hands_tone5",
-    "unicode": "1F64C-1F3FF",
+  "raised_hands_tone5": {
+    "category": "people",
+    "moji": "🙌🏿",
+    "unicodeVersion": "8.0",
     "digest": "2a5ed7334a17172db0cd820a559e7f75df40ec44de6c25d194c76e1b58c634cb"
   },
-  {
-    "name": "raising_hand",
-    "unicode": "1F64B",
+  "raising_hand": {
+    "category": "people",
+    "moji": "🙋",
+    "unicodeVersion": "6.0",
     "digest": "512750b00704f1ccefd3c757743540b785ad7670dbbe4a2c4dca8d93e6701920"
   },
-  {
-    "name": "raising_hand_tone1",
-    "unicode": "1F64B-1F3FB",
+  "raising_hand_tone1": {
+    "category": "people",
+    "moji": "🙋🏻",
+    "unicodeVersion": "8.0",
     "digest": "2897722f091c273dd3714cff7423c2475bc3070416c28014ca03322b9ece48bc"
   },
-  {
-    "name": "raising_hand_tone2",
-    "unicode": "1F64B-1F3FC",
+  "raising_hand_tone2": {
+    "category": "people",
+    "moji": "🙋🏼",
+    "unicodeVersion": "8.0",
     "digest": "59199b334b3845911382c1f29bd7c0d5ef9d2486417345e265b166ead7d3e1c1"
   },
-  {
-    "name": "raising_hand_tone3",
-    "unicode": "1F64B-1F3FD",
+  "raising_hand_tone3": {
+    "category": "people",
+    "moji": "🙋🏽",
+    "unicodeVersion": "8.0",
     "digest": "f95b338d5efcf14ef12f415a2c1bba93df48628ddc94f34f70c31e1b3c2e1d28"
   },
-  {
-    "name": "raising_hand_tone4",
-    "unicode": "1F64B-1F3FE",
+  "raising_hand_tone4": {
+    "category": "people",
+    "moji": "🙋🏾",
+    "unicodeVersion": "8.0",
     "digest": "951ddbfdb57d5a60551b59b3d0f7ca00a64912f4a101a73afaebd68445cd6cec"
   },
-  {
-    "name": "raising_hand_tone5",
-    "unicode": "1F64B-1F3FF",
+  "raising_hand_tone5": {
+    "category": "people",
+    "moji": "🙋🏿",
+    "unicodeVersion": "8.0",
     "digest": "9370f93704d8f89ca6dc946715eab5e7dba82bf04dd68c00f5c0abb8bc16371e"
   },
-  {
-    "name": "ram",
-    "unicode": "1F40F",
+  "ram": {
+    "category": "nature",
+    "moji": "🐏",
+    "unicodeVersion": "6.0",
     "digest": "2875ab28e1018b39062aeb0c5ce488c48a98f13e9f2364470a0a700b126604f2"
   },
-  {
-    "name": "ramen",
-    "unicode": "1F35C",
+  "ramen": {
+    "category": "food",
+    "moji": "🍜",
+    "unicodeVersion": "6.0",
     "digest": "425662a49c4c13577c0de8d45d004e5ba204aaadbaabae62a5c283ecd7a9a2c5"
   },
-  {
-    "name": "rat",
-    "unicode": "1F400",
+  "rat": {
+    "category": "nature",
+    "moji": "🐀",
+    "unicodeVersion": "6.0",
     "digest": "14380d65498c6ce037c02a93bca2b24f25a368d85278d6015b8c9f7cd261f8e2"
   },
-  {
-    "name": "record_button",
-    "unicode": "23FA",
+  "record_button": {
+    "category": "symbols",
+    "moji": "⏺",
+    "unicodeVersion": "7.0",
     "digest": "92be12161ba206bb2e06a39131711c7b17368d55b4aae0b48f0ac5b6b1cde76b"
   },
-  {
-    "name": "recycle",
-    "unicode": "267B",
+  "recycle": {
+    "category": "symbols",
+    "moji": "♻",
+    "unicodeVersion": "3.2",
     "digest": "c377e8537367b05b5de9be860a0fcabd7aed2bf4ba146eefc423671a21530369"
   },
-  {
-    "name": "red_car",
-    "unicode": "1F697",
+  "red_car": {
+    "category": "travel",
+    "moji": "🚗",
+    "unicodeVersion": "6.0",
     "digest": "8a99832a195263c0e922af53d52dea37aa3e07032b3c2a1977f8527b4a144b9c"
   },
-  {
-    "name": "red_circle",
-    "unicode": "1F534",
+  "red_circle": {
+    "category": "symbols",
+    "moji": "🔴",
+    "unicodeVersion": "6.0",
     "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307"
   },
-  {
-    "name": "registered",
-    "unicode": "00AE",
+  "registered": {
+    "category": "symbols",
+    "moji": "®",
+    "unicodeVersion": "1.1",
     "digest": "9661b1df529ecb752d130820c55c403e5de263748eb02f7fea327818bc282d94"
   },
-  {
-    "name": "relaxed",
-    "unicode": "263A",
+  "relaxed": {
+    "category": "people",
+    "moji": "☺",
+    "unicodeVersion": "1.1",
     "digest": "2d5aed4fb8504c6d6660ef8d3bfe0cc053dcd6099c2f53748c202dc970c639bc"
   },
-  {
-    "name": "relieved",
-    "unicode": "1F60C",
+  "relieved": {
+    "category": "people",
+    "moji": "😌",
+    "unicodeVersion": "6.0",
     "digest": "b4ce2ba6c220d887fe5e333c05ed773df9b6df0ac456879fd8f5103ff68604a5"
   },
-  {
-    "name": "reminder_ribbon",
-    "unicode": "1F397",
+  "reminder_ribbon": {
+    "category": "activity",
+    "moji": "🎗",
+    "unicodeVersion": "7.0",
     "digest": "c3de2a7c9350b77a0b86c0dcce9dcd9953ea8a97aa1e7aed149755924742f54d"
   },
-  {
-    "name": "repeat",
-    "unicode": "1F501",
+  "repeat": {
+    "category": "symbols",
+    "moji": "🔁",
+    "unicodeVersion": "6.0",
     "digest": "b9512d508613ed0eb3181eb1030f7f6fd6b994476ecdfa308733c6df975fb99e"
   },
-  {
-    "name": "repeat_one",
-    "unicode": "1F502",
+  "repeat_one": {
+    "category": "symbols",
+    "moji": "🔂",
+    "unicodeVersion": "6.0",
     "digest": "53409cf24dd4bb0d7b50ae359f15d06b87b7f4a292ed5c3a09652fa421a90bf2"
   },
-  {
-    "name": "restroom",
-    "unicode": "1F6BB",
+  "restroom": {
+    "category": "symbols",
+    "moji": "🚻",
+    "unicodeVersion": "6.0",
     "digest": "2e7a1bfc9a9d49b0272230a91db7369e24d54bf1de8e683d36b85f1d8c037f77"
   },
-  {
-    "name": "revolving_hearts",
-    "unicode": "1F49E",
+  "revolving_hearts": {
+    "category": "symbols",
+    "moji": "💞",
+    "unicodeVersion": "6.0",
     "digest": "c43d3197cb4cf06659f643638f6c4e91a2889e0f6531b7d81ea826c2a8b784fc"
   },
-  {
-    "name": "rewind",
-    "unicode": "23EA",
+  "rewind": {
+    "category": "symbols",
+    "moji": "⏪",
+    "unicodeVersion": "6.0",
     "digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd"
   },
-  {
-    "name": "rhino",
-    "unicode": "1F98F",
-    "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
-  },
-  {
-    "name": "rhinoceros",
-    "unicode": "1F98F",
+  "rhino": {
+    "category": "nature",
+    "moji": "🦏",
+    "unicodeVersion": "9.0",
     "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
   },
-  {
-    "name": "ribbon",
-    "unicode": "1F380",
+  "ribbon": {
+    "category": "objects",
+    "moji": "🎀",
+    "unicodeVersion": "6.0",
     "digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828"
   },
-  {
-    "name": "rice",
-    "unicode": "1F35A",
+  "rice": {
+    "category": "food",
+    "moji": "🍚",
+    "unicodeVersion": "6.0",
     "digest": "f544f12606de59d28739798003f14ebd8869856add8e24496ec5dda3e131daf4"
   },
-  {
-    "name": "rice_ball",
-    "unicode": "1F359",
+  "rice_ball": {
+    "category": "food",
+    "moji": "🍙",
+    "unicodeVersion": "6.0",
     "digest": "2cba6f5364cd366859bc8948897b65fc97b225ea7973d9be3b24aba388fed8e8"
   },
-  {
-    "name": "rice_cracker",
-    "unicode": "1F358",
+  "rice_cracker": {
+    "category": "food",
+    "moji": "🍘",
+    "unicodeVersion": "6.0",
     "digest": "ac0f805d41d4f322154c1968bd3ce3e9aabcd39d908182e52fd7d28458dbef92"
   },
-  {
-    "name": "rice_scene",
-    "unicode": "1F391",
+  "rice_scene": {
+    "category": "travel",
+    "moji": "🎑",
+    "unicodeVersion": "6.0",
     "digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5"
   },
-  {
-    "name": "right_facing_fist",
-    "unicode": "1F91C",
+  "right_facing_fist": {
+    "category": "people",
+    "moji": "🤜",
+    "unicodeVersion": "9.0",
     "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
   },
-  {
-    "name": "right_fist",
-    "unicode": "1F91C",
-    "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
-  },
-  {
-    "name": "right_facing_fist_tone1",
-    "unicode": "1F91C-1F3FB",
-    "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
-  },
-  {
-    "name": "right_fist_tone1",
-    "unicode": "1F91C-1F3FB",
+  "right_facing_fist_tone1": {
+    "category": "people",
+    "moji": "🤜🏻",
+    "unicodeVersion": "9.0",
     "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
   },
-  {
-    "name": "right_facing_fist_tone2",
-    "unicode": "1F91C-1F3FC",
+  "right_facing_fist_tone2": {
+    "category": "people",
+    "moji": "🤜🏼",
+    "unicodeVersion": "9.0",
     "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
   },
-  {
-    "name": "right_fist_tone2",
-    "unicode": "1F91C-1F3FC",
-    "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
-  },
-  {
-    "name": "right_facing_fist_tone3",
-    "unicode": "1F91C-1F3FD",
-    "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
-  },
-  {
-    "name": "right_fist_tone3",
-    "unicode": "1F91C-1F3FD",
+  "right_facing_fist_tone3": {
+    "category": "people",
+    "moji": "🤜🏽",
+    "unicodeVersion": "9.0",
     "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
   },
-  {
-    "name": "right_facing_fist_tone4",
-    "unicode": "1F91C-1F3FE",
+  "right_facing_fist_tone4": {
+    "category": "people",
+    "moji": "🤜🏾",
+    "unicodeVersion": "9.0",
     "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
   },
-  {
-    "name": "right_fist_tone4",
-    "unicode": "1F91C-1F3FE",
-    "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
-  },
-  {
-    "name": "right_facing_fist_tone5",
-    "unicode": "1F91C-1F3FF",
-    "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
-  },
-  {
-    "name": "right_fist_tone5",
-    "unicode": "1F91C-1F3FF",
+  "right_facing_fist_tone5": {
+    "category": "people",
+    "moji": "🤜🏿",
+    "unicodeVersion": "9.0",
     "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
   },
-  {
-    "name": "ring",
-    "unicode": "1F48D",
+  "ring": {
+    "category": "people",
+    "moji": "💍",
+    "unicodeVersion": "6.0",
     "digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d"
   },
-  {
-    "name": "robot",
-    "unicode": "1F916",
+  "robot": {
+    "category": "people",
+    "moji": "🤖",
+    "unicodeVersion": "8.0",
     "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
   },
-  {
-    "name": "robot_face",
-    "unicode": "1F916",
-    "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
-  },
-  {
-    "name": "rocket",
-    "unicode": "1F680",
+  "rocket": {
+    "category": "travel",
+    "moji": "🚀",
+    "unicodeVersion": "6.0",
     "digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d"
   },
-  {
-    "name": "rofl",
-    "unicode": "1F923",
-    "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
-  },
-  {
-    "name": "rolling_on_the_floor_laughing",
-    "unicode": "1F923",
+  "rofl": {
+    "category": "people",
+    "moji": "🤣",
+    "unicodeVersion": "9.0",
     "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
   },
-  {
-    "name": "roller_coaster",
-    "unicode": "1F3A2",
+  "roller_coaster": {
+    "category": "travel",
+    "moji": "🎢",
+    "unicodeVersion": "6.0",
     "digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c"
   },
-  {
-    "name": "rolling_eyes",
-    "unicode": "1F644",
+  "rolling_eyes": {
+    "category": "people",
+    "moji": "🙄",
+    "unicodeVersion": "8.0",
     "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
   },
-  {
-    "name": "face_with_rolling_eyes",
-    "unicode": "1F644",
-    "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
-  },
-  {
-    "name": "rooster",
-    "unicode": "1F413",
+  "rooster": {
+    "category": "nature",
+    "moji": "🐓",
+    "unicodeVersion": "6.0",
     "digest": "2b90c5cf6fa46da13eb77285443d600afcea0c48bd1d215d60167e7dc510da5d"
   },
-  {
-    "name": "rose",
-    "unicode": "1F339",
+  "rose": {
+    "category": "nature",
+    "moji": "🌹",
+    "unicodeVersion": "6.0",
     "digest": "73799e459dba188de4de704605d824242feeb65d587c5bf9109acf528d037146"
   },
-  {
-    "name": "rosette",
-    "unicode": "1F3F5",
+  "rosette": {
+    "category": "activity",
+    "moji": "🏵",
+    "unicodeVersion": "7.0",
     "digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5"
   },
-  {
-    "name": "rotating_light",
-    "unicode": "1F6A8",
+  "rotating_light": {
+    "category": "travel",
+    "moji": "🚨",
+    "unicodeVersion": "6.0",
     "digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f"
   },
-  {
-    "name": "round_pushpin",
-    "unicode": "1F4CD",
+  "round_pushpin": {
+    "category": "objects",
+    "moji": "📍",
+    "unicodeVersion": "6.0",
     "digest": "8ffca77bbdc6f1f726daf3abd6eff338a5ad1aa9b09dbbd8782c1e7ef5452f30"
   },
-  {
-    "name": "rowboat",
-    "unicode": "1F6A3",
+  "rowboat": {
+    "category": "activity",
+    "moji": "🚣",
+    "unicodeVersion": "6.0",
     "digest": "83715d83a061926d4ad3bb569b21f5d337e3ebd4c9bcdfe493e661c12adc0a16"
   },
-  {
-    "name": "rowboat_tone1",
-    "unicode": "1F6A3-1F3FB",
+  "rowboat_tone1": {
+    "category": "activity",
+    "moji": "🚣🏻",
+    "unicodeVersion": "8.0",
     "digest": "e279ac816442c0876fba1f42c700b80f2fb6de671e1a8a9e9d11b71eed5c58e8"
   },
-  {
-    "name": "rowboat_tone2",
-    "unicode": "1F6A3-1F3FC",
+  "rowboat_tone2": {
+    "category": "activity",
+    "moji": "🚣🏼",
+    "unicodeVersion": "8.0",
     "digest": "6a48eba352ed4971d26498b6c622e5772389c89c5205ed02acde8e995dddcc3b"
   },
-  {
-    "name": "rowboat_tone3",
-    "unicode": "1F6A3-1F3FD",
+  "rowboat_tone3": {
+    "category": "activity",
+    "moji": "🚣🏽",
+    "unicodeVersion": "8.0",
     "digest": "875948f6d8354ebd95ce9a66fde30f06a8366dcd89d5ca3e660845f8801e9305"
   },
-  {
-    "name": "rowboat_tone4",
-    "unicode": "1F6A3-1F3FE",
+  "rowboat_tone4": {
+    "category": "activity",
+    "moji": "🚣🏾",
+    "unicodeVersion": "8.0",
     "digest": "8c7ac7346b0020d0ff5e2f4a1efb1b7785eac637f17556663ec33e2335083f0a"
   },
-  {
-    "name": "rowboat_tone5",
-    "unicode": "1F6A3-1F3FF",
+  "rowboat_tone5": {
+    "category": "activity",
+    "moji": "🚣🏿",
+    "unicodeVersion": "8.0",
     "digest": "a399dbb647892b22323e0bf17bc36a9b5f1708ebedf9ba525233ee7b9d48339a"
   },
-  {
-    "name": "rugby_football",
-    "unicode": "1F3C9",
+  "rugby_football": {
+    "category": "activity",
+    "moji": "🏉",
+    "unicodeVersion": "6.0",
     "digest": "cc6f00ade3e0bbb7899e7bfb138b57216dd66de26d7967d5ffa501f382ed09f4"
   },
-  {
-    "name": "runner",
-    "unicode": "1F3C3",
+  "runner": {
+    "category": "people",
+    "moji": "🏃",
+    "unicodeVersion": "6.0",
     "digest": "e9af7b591be60ade2049dbada0f062ba2d3e17f02bec76cbd34ce68854a2a10c"
   },
-  {
-    "name": "runner_tone1",
-    "unicode": "1F3C3-1F3FB",
+  "runner_tone1": {
+    "category": "people",
+    "moji": "🏃🏻",
+    "unicodeVersion": "8.0",
     "digest": "21091cbb09c558712ecf63548bf28b7995df42bdb85235088799a517800e52f5"
   },
-  {
-    "name": "runner_tone2",
-    "unicode": "1F3C3-1F3FC",
+  "runner_tone2": {
+    "category": "people",
+    "moji": "🏃🏼",
+    "unicodeVersion": "8.0",
     "digest": "1fe3d194f675a46fe67799394192e66c407dd81163363692c5e7da32ddb9af2b"
   },
-  {
-    "name": "runner_tone3",
-    "unicode": "1F3C3-1F3FD",
+  "runner_tone3": {
+    "category": "people",
+    "moji": "🏃🏽",
+    "unicodeVersion": "8.0",
     "digest": "8cea1bf4ef3be71f42dc5bae978d5b7a197a3851543225349ef0dda29a370537"
   },
-  {
-    "name": "runner_tone4",
-    "unicode": "1F3C3-1F3FE",
+  "runner_tone4": {
+    "category": "people",
+    "moji": "🏃🏾",
+    "unicodeVersion": "8.0",
     "digest": "c33f0b8b5a71d295fb6ba322e79446964a8eca9e4573efd591e4273808b088a0"
   },
-  {
-    "name": "runner_tone5",
-    "unicode": "1F3C3-1F3FF",
+  "runner_tone5": {
+    "category": "people",
+    "moji": "🏃🏿",
+    "unicodeVersion": "8.0",
     "digest": "9f59e6dd0fdf2f17bceb41f5c355b4e6f3c8bb8cbd8af0992f0b5630ff8892e8"
   },
-  {
-    "name": "running_shirt_with_sash",
-    "unicode": "1F3BD",
+  "running_shirt_with_sash": {
+    "category": "activity",
+    "moji": "🎽",
+    "unicodeVersion": "6.0",
     "digest": "7542307d3595aca45e8ccae66b6e58b6e92870144b738263d5379ec6dc992b76"
   },
-  {
-    "name": "sa",
-    "unicode": "1F202",
+  "sa": {
+    "category": "symbols",
+    "moji": "🈂",
+    "unicodeVersion": "6.0",
     "digest": "6042bcabd1516ef3847d695aba22851c49421244432d256e24eba04e8a223dab"
   },
-  {
-    "name": "sagittarius",
-    "unicode": "2650",
+  "sagittarius": {
+    "category": "symbols",
+    "moji": "♐",
+    "unicodeVersion": "1.1",
     "digest": "a02593e025023f2e82a01c587a8c0bbb1eff88cbcabf535a1558413eb32ed1d5"
   },
-  {
-    "name": "sailboat",
-    "unicode": "26F5",
+  "sailboat": {
+    "category": "travel",
+    "moji": "⛵",
+    "unicodeVersion": "5.2",
     "digest": "c95ef4dc939cbdcb757ef3cd90331310e8c0a426add8cc800bae2540148a3195"
   },
-  {
-    "name": "sake",
-    "unicode": "1F376",
+  "sake": {
+    "category": "food",
+    "moji": "🍶",
+    "unicodeVersion": "6.0",
     "digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4"
   },
-  {
-    "name": "salad",
-    "unicode": "1F957",
-    "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
-  },
-  {
-    "name": "green_salad",
-    "unicode": "1F957",
+  "salad": {
+    "category": "food",
+    "moji": "🥗",
+    "unicodeVersion": "9.0",
     "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
   },
-  {
-    "name": "sandal",
-    "unicode": "1F461",
+  "sandal": {
+    "category": "people",
+    "moji": "👡",
+    "unicodeVersion": "6.0",
     "digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d"
   },
-  {
-    "name": "santa",
-    "unicode": "1F385",
+  "santa": {
+    "category": "people",
+    "moji": "🎅",
+    "unicodeVersion": "6.0",
     "digest": "178513e3d815917e59958870f5885b3414b43a16b8056980c863a468dfe00179"
   },
-  {
-    "name": "santa_tone1",
-    "unicode": "1F385-1F3FB",
+  "santa_tone1": {
+    "category": "people",
+    "moji": "🎅🏻",
+    "unicodeVersion": "8.0",
     "digest": "bf900bbc19bbd329229add9326e28e8197b69d6ddceb69f42162b0200fde5d16"
   },
-  {
-    "name": "santa_tone2",
-    "unicode": "1F385-1F3FC",
+  "santa_tone2": {
+    "category": "people",
+    "moji": "🎅🏼",
+    "unicodeVersion": "8.0",
     "digest": "7340f2171adab97198e3eecac8b0d84c4c2a41f84606301a0d10e9fe655c93d1"
   },
-  {
-    "name": "santa_tone3",
-    "unicode": "1F385-1F3FD",
+  "santa_tone3": {
+    "category": "people",
+    "moji": "🎅🏽",
+    "unicodeVersion": "8.0",
     "digest": "7368ab75454ec28d8f7d6baef6ad69b5278445a9f50753f6624731bffde32054"
   },
-  {
-    "name": "santa_tone4",
-    "unicode": "1F385-1F3FE",
+  "santa_tone4": {
+    "category": "people",
+    "moji": "🎅🏾",
+    "unicodeVersion": "8.0",
     "digest": "0ee60188353e0ee7772079c192bebbc6d49e74e63906f840c66da4eb35f4f245"
   },
-  {
-    "name": "santa_tone5",
-    "unicode": "1F385-1F3FF",
+  "santa_tone5": {
+    "category": "people",
+    "moji": "🎅🏿",
+    "unicodeVersion": "8.0",
     "digest": "e4378a0cc5d21e9b9fe6e35c32d1ebc6fb8c2e1c09554cd096aeaefd3a6eb511"
   },
-  {
-    "name": "satellite",
-    "unicode": "1F4E1",
+  "satellite": {
+    "category": "objects",
+    "moji": "📡",
+    "unicodeVersion": "6.0",
     "digest": "c9d63118dcb445856917bb080460ab695cc78e715dcbba30ba18dffa9e906b27"
   },
-  {
-    "name": "satellite_orbital",
-    "unicode": "1F6F0",
+  "satellite_orbital": {
+    "category": "travel",
+    "moji": "🛰",
+    "unicodeVersion": "7.0",
     "digest": "beb2f50e7f2b010e76bed9daa95d7329a93c783d3ebc4f0b797dd721c5e3d32d"
   },
-  {
-    "name": "saxophone",
-    "unicode": "1F3B7",
+  "saxophone": {
+    "category": "activity",
+    "moji": "🎷",
+    "unicodeVersion": "6.0",
     "digest": "dfd138634f6702a3b89b5a2a50016720eef3f800b0d1d8c9fe097808c9491e96"
   },
-  {
-    "name": "scales",
-    "unicode": "2696",
+  "scales": {
+    "category": "objects",
+    "moji": "⚖",
+    "unicodeVersion": "4.1",
     "digest": "2280c026f16c6b92e0daa00bc14e718770f8d231c571ab439bde84d837cf31cc"
   },
-  {
-    "name": "school",
-    "unicode": "1F3EB",
+  "school": {
+    "category": "travel",
+    "moji": "🏫",
+    "unicodeVersion": "6.0",
     "digest": "af198b068a86ccad3daec4c6873e6b4735086c1ecbb3848182e70bae9aa3ee24"
   },
-  {
-    "name": "school_satchel",
-    "unicode": "1F392",
+  "school_satchel": {
+    "category": "people",
+    "moji": "🎒",
+    "unicodeVersion": "6.0",
     "digest": "f670ae8aea67eb9d8aaa0bf2748c1cc3e503dcc1dbe999133afcdf21af046b24"
   },
-  {
-    "name": "scissors",
-    "unicode": "2702",
+  "scissors": {
+    "category": "objects",
+    "moji": "✂",
+    "unicodeVersion": "1.1",
     "digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8"
   },
-  {
-    "name": "scooter",
-    "unicode": "1F6F4",
+  "scooter": {
+    "category": "travel",
+    "moji": "🛴",
+    "unicodeVersion": "9.0",
     "digest": "4a7db148880398db75e059711cb53edefb6b8fa9d442009f52856b887ab1dde4"
   },
-  {
-    "name": "scorpion",
-    "unicode": "1F982",
+  "scorpion": {
+    "category": "nature",
+    "moji": "🦂",
+    "unicodeVersion": "8.0",
     "digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a"
   },
-  {
-    "name": "scorpius",
-    "unicode": "264F",
+  "scorpius": {
+    "category": "symbols",
+    "moji": "♏",
+    "unicodeVersion": "1.1",
     "digest": "a36404b408814c2ecb8fa8b61f5c5432dfcf54cae8c09cc67b8d0fadf7cbdc03"
   },
-  {
-    "name": "scream",
-    "unicode": "1F631",
+  "scream": {
+    "category": "people",
+    "moji": "😱",
+    "unicodeVersion": "6.0",
     "digest": "916e4903a4b694da4b00f190f872a4e100e7736b7a2e6171fa1636f46bf646e6"
   },
-  {
-    "name": "scream_cat",
-    "unicode": "1F640",
+  "scream_cat": {
+    "category": "people",
+    "moji": "🙀",
+    "unicodeVersion": "6.0",
     "digest": "f1d3a6ff538064e7d5e0321bbc33aba44e8da703dc1894ef1403c0cd6d63d781"
   },
-  {
-    "name": "scroll",
-    "unicode": "1F4DC",
+  "scroll": {
+    "category": "objects",
+    "moji": "📜",
+    "unicodeVersion": "6.0",
     "digest": "9b2cb00860bcc2d20017cafb2ed9681b6232dc07273d489d75d53ce29e4ba3ab"
   },
-  {
-    "name": "seat",
-    "unicode": "1F4BA",
+  "seat": {
+    "category": "travel",
+    "moji": "💺",
+    "unicodeVersion": "6.0",
     "digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1"
   },
-  {
-    "name": "second_place",
-    "unicode": "1F948",
+  "second_place": {
+    "category": "activity",
+    "moji": "🥈",
+    "unicodeVersion": "9.0",
     "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
   },
-  {
-    "name": "second_place_medal",
-    "unicode": "1F948",
-    "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
-  },
-  {
-    "name": "secret",
-    "unicode": "3299",
+  "secret": {
+    "category": "symbols",
+    "moji": "㊙",
+    "unicodeVersion": "1.1",
     "digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0"
   },
-  {
-    "name": "see_no_evil",
-    "unicode": "1F648",
+  "see_no_evil": {
+    "category": "nature",
+    "moji": "🙈",
+    "unicodeVersion": "6.0",
     "digest": "3ff66d2e84b36d071d0a34f8e41cfd620a56b83131474ea50ed7803b635551ed"
   },
-  {
-    "name": "seedling",
-    "unicode": "1F331",
+  "seedling": {
+    "category": "nature",
+    "moji": "🌱",
+    "unicodeVersion": "6.0",
     "digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75"
   },
-  {
-    "name": "selfie",
-    "unicode": "1F933",
+  "selfie": {
+    "category": "people",
+    "moji": "🤳",
+    "unicodeVersion": "9.0",
     "digest": "2a1bc9f18ad4d6fb893d91c88ef1b2d9bd063dc2bb1a4b08c248c30f52545d4e"
   },
-  {
-    "name": "selfie_tone1",
-    "unicode": "1F933-1F3FB",
+  "selfie_tone1": {
+    "category": "people",
+    "moji": "🤳🏻",
+    "unicodeVersion": "9.0",
     "digest": "26dc212ffed30c276bd6a66a72bc4513e68098a2205fb4ca5b51ccfa1de5b544"
   },
-  {
-    "name": "selfie_tone2",
-    "unicode": "1F933-1F3FC",
+  "selfie_tone2": {
+    "category": "people",
+    "moji": "🤳🏼",
+    "unicodeVersion": "9.0",
     "digest": "71eceaefda46e3521f374f76693e7fa8f215067498067900080e2925ca94d7de"
   },
-  {
-    "name": "selfie_tone3",
-    "unicode": "1F933-1F3FD",
+  "selfie_tone3": {
+    "category": "people",
+    "moji": "🤳🏽",
+    "unicodeVersion": "9.0",
     "digest": "53eabbd4f6b8ebbd2f7af7bf5cd64309c4039ac1c5b2180290a547deaafcebdf"
   },
-  {
-    "name": "selfie_tone4",
-    "unicode": "1F933-1F3FE",
+  "selfie_tone4": {
+    "category": "people",
+    "moji": "🤳🏾",
+    "unicodeVersion": "9.0",
     "digest": "0baad378b09652b99c5d458db2e03b4db14a1557db4ea0969806a0ca1d33d40c"
   },
-  {
-    "name": "selfie_tone5",
-    "unicode": "1F933-1F3FF",
+  "selfie_tone5": {
+    "category": "people",
+    "moji": "🤳🏿",
+    "unicodeVersion": "9.0",
     "digest": "9a07608f34ec4dad48764a855f83f3965709d7b2fd2342e6dc9ed61f23f4adfd"
   },
-  {
-    "name": "seven",
-    "unicode": "0037-20E3",
+  "seven": {
+    "category": "symbols",
+    "moji": "7️⃣",
+    "unicodeVersion": "3.0",
     "digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2"
   },
-  {
-    "name": "shallow_pan_of_food",
-    "unicode": "1F958",
-    "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
-  },
-  {
-    "name": "paella",
-    "unicode": "1F958",
+  "shallow_pan_of_food": {
+    "category": "food",
+    "moji": "🥘",
+    "unicodeVersion": "9.0",
     "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
   },
-  {
-    "name": "shamrock",
-    "unicode": "2618",
+  "shamrock": {
+    "category": "nature",
+    "moji": "☘",
+    "unicodeVersion": "4.1",
     "digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488"
   },
-  {
-    "name": "shark",
-    "unicode": "1F988",
+  "shark": {
+    "category": "nature",
+    "moji": "🦈",
+    "unicodeVersion": "9.0",
     "digest": "23a2364b6356e7bbb84c138e9cf58e2c68cd8caabb337a0c4d365ce87bf5d2da"
   },
-  {
-    "name": "shaved_ice",
-    "unicode": "1F367",
+  "shaved_ice": {
+    "category": "food",
+    "moji": "🍧",
+    "unicodeVersion": "6.0",
     "digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74"
   },
-  {
-    "name": "sheep",
-    "unicode": "1F411",
+  "sheep": {
+    "category": "nature",
+    "moji": "🐑",
+    "unicodeVersion": "6.0",
     "digest": "c867c8e6e51768f1f51f4fe5abd3fbd5c1d69b01a3cb48b5fb94b6e2338a271c"
   },
-  {
-    "name": "shell",
-    "unicode": "1F41A",
+  "shell": {
+    "category": "nature",
+    "moji": "🐚",
+    "unicodeVersion": "6.0",
     "digest": "8983652d33ad6ab91195518cecb5a268a1c0ae603d271f0ddd756ff50058ddb3"
   },
-  {
-    "name": "shield",
-    "unicode": "1F6E1",
+  "shield": {
+    "category": "objects",
+    "moji": "🛡",
+    "unicodeVersion": "7.0",
     "digest": "763d0a56a62c51c730ccb0fbea38ab597cbf41a85ab968198e6ec35630d50aa5"
   },
-  {
-    "name": "shinto_shrine",
-    "unicode": "26E9",
+  "shinto_shrine": {
+    "category": "travel",
+    "moji": "⛩",
+    "unicodeVersion": "5.2",
     "digest": "38a6d756c5aa9703510afa5076d75192f7814bbb6632394d4b8253d9ceda7f8c"
   },
-  {
-    "name": "ship",
-    "unicode": "1F6A2",
+  "ship": {
+    "category": "travel",
+    "moji": "🚢",
+    "unicodeVersion": "6.0",
     "digest": "79c680845892a3e81ec6af2160ee07c29147155943e5daba6c76d04252014c20"
   },
-  {
-    "name": "shirt",
-    "unicode": "1F455",
+  "shirt": {
+    "category": "people",
+    "moji": "👕",
+    "unicodeVersion": "6.0",
     "digest": "46c7253e15d7cac03699ddb1550fbb7565bbe487310f7e218c0583aa69f9d3c5"
   },
-  {
-    "name": "shopping_bags",
-    "unicode": "1F6CD",
+  "shopping_bags": {
+    "category": "objects",
+    "moji": "🛍",
+    "unicodeVersion": "7.0",
     "digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b"
   },
-  {
-    "name": "shopping_cart",
-    "unicode": "1F6D2",
+  "shopping_cart": {
+    "category": "objects",
+    "moji": "🛒",
+    "unicodeVersion": "9.0",
     "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
   },
-  {
-    "name": "shopping_trolley",
-    "unicode": "1F6D2",
-    "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
-  },
-  {
-    "name": "shower",
-    "unicode": "1F6BF",
+  "shower": {
+    "category": "objects",
+    "moji": "🚿",
+    "unicodeVersion": "6.0",
     "digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01"
   },
-  {
-    "name": "shrimp",
-    "unicode": "1F990",
+  "shrimp": {
+    "category": "nature",
+    "moji": "🦐",
+    "unicodeVersion": "9.0",
     "digest": "b3651f3be3767125076a013fe903854f5b456a8afae865cb219cf528e0f44caa"
   },
-  {
-    "name": "shrug",
-    "unicode": "1F937",
+  "shrug": {
+    "category": "people",
+    "moji": "🤷",
+    "unicodeVersion": "9.0",
     "digest": "6e264243cc3b6e396069dea4357a958bdcd4081cb1af0ed6aa47235bef88cf27"
   },
-  {
-    "name": "shrug_tone1",
-    "unicode": "1F937-1F3FB",
+  "shrug_tone1": {
+    "category": "people",
+    "moji": "🤷🏻",
+    "unicodeVersion": "9.0",
     "digest": "0567b9fd95c8a857914003a5465a500ca79c8111811d45b865021b1b1d92d0b1"
   },
-  {
-    "name": "shrug_tone2",
-    "unicode": "1F937-1F3FC",
+  "shrug_tone2": {
+    "category": "people",
+    "moji": "🤷🏼",
+    "unicodeVersion": "9.0",
     "digest": "1557c2f5e3d4599c806d74c0b78afcca940678787534b6862bb89a20601bac8a"
   },
-  {
-    "name": "shrug_tone3",
-    "unicode": "1F937-1F3FD",
+  "shrug_tone3": {
+    "category": "people",
+    "moji": "🤷🏽",
+    "unicodeVersion": "9.0",
     "digest": "f02754541a7bf74ba7eebe6c27daf1e3e1dac25172c35b8ba45641e278dfda3d"
   },
-  {
-    "name": "shrug_tone4",
-    "unicode": "1F937-1F3FE",
+  "shrug_tone4": {
+    "category": "people",
+    "moji": "🤷🏾",
+    "unicodeVersion": "9.0",
     "digest": "2b5121164cb5f4e253d8fb31f6445cf8afaf30dba41732edc511440cdb78d15c"
   },
-  {
-    "name": "shrug_tone5",
-    "unicode": "1F937-1F3FF",
+  "shrug_tone5": {
+    "category": "people",
+    "moji": "🤷🏿",
+    "unicodeVersion": "9.0",
     "digest": "62d99a26bbad479f574f66208c41b9960cd41fb9d79d3a13fbdaa44682077115"
   },
-  {
-    "name": "signal_strength",
-    "unicode": "1F4F6",
+  "signal_strength": {
+    "category": "symbols",
+    "moji": "📶",
+    "unicodeVersion": "6.0",
     "digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447"
   },
-  {
-    "name": "six",
-    "unicode": "0036-20E3",
+  "six": {
+    "category": "symbols",
+    "moji": "6️⃣",
+    "unicodeVersion": "3.0",
     "digest": "cede9324261208d0fd5d00fcdfc0df0331944bd9cff4f40b30a582a641526c1c"
   },
-  {
-    "name": "six_pointed_star",
-    "unicode": "1F52F",
+  "six_pointed_star": {
+    "category": "symbols",
+    "moji": "🔯",
+    "unicodeVersion": "6.0",
     "digest": "9203e3b4f08af439ae0bfb6a7b29a02dceb027b6c2dc5463b524dfd314cbff4e"
   },
-  {
-    "name": "ski",
-    "unicode": "1F3BF",
+  "ski": {
+    "category": "activity",
+    "moji": "🎿",
+    "unicodeVersion": "6.0",
     "digest": "80f0ca8660ba373fef823af9e98e148c4ddb1e217eb6d0a0ea2bae2288b57570"
   },
-  {
-    "name": "skier",
-    "unicode": "26F7",
+  "skier": {
+    "category": "activity",
+    "moji": "⛷",
+    "unicodeVersion": "5.2",
     "digest": "4fff0aa155367f551a59aed9657b8afa159173882b25db9cd8434293d1eed76d"
   },
-  {
-    "name": "skull",
-    "unicode": "1F480",
-    "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
-  },
-  {
-    "name": "skeleton",
-    "unicode": "1F480",
+  "skull": {
+    "category": "people",
+    "moji": "💀",
+    "unicodeVersion": "6.0",
     "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
   },
-  {
-    "name": "skull_crossbones",
-    "unicode": "2620",
+  "skull_crossbones": {
+    "category": "objects",
+    "moji": "☠",
+    "unicodeVersion": "1.1",
     "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
   },
-  {
-    "name": "skull_and_crossbones",
-    "unicode": "2620",
-    "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
-  },
-  {
-    "name": "sleeping",
-    "unicode": "1F634",
+  "sleeping": {
+    "category": "people",
+    "moji": "😴",
+    "unicodeVersion": "6.1",
     "digest": "1050a011509b56735c9f30a6fccc876256e2a4546dc6052e518151c8aca4b526"
   },
-  {
-    "name": "sleeping_accommodation",
-    "unicode": "1F6CC",
+  "sleeping_accommodation": {
+    "category": "objects",
+    "moji": "🛌",
+    "unicodeVersion": "7.0",
     "digest": "2ce42c027d1d0947abc403c359fd668a7bc44f5ead2582e97f3db7dd4e22e5d5"
   },
-  {
-    "name": "sleepy",
-    "unicode": "1F62A",
+  "sleepy": {
+    "category": "people",
+    "moji": "😪",
+    "unicodeVersion": "6.0",
     "digest": "2ee9bb1f72ef99e0e33095ec2bbf7a58ffea0ff7d40b840f4cdba57be9de74b0"
   },
-  {
-    "name": "slight_frown",
-    "unicode": "1F641",
-    "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
-  },
-  {
-    "name": "slightly_frowning_face",
-    "unicode": "1F641",
+  "slight_frown": {
+    "category": "people",
+    "moji": "🙁",
+    "unicodeVersion": "7.0",
     "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
   },
-  {
-    "name": "slight_smile",
-    "unicode": "1F642",
+  "slight_smile": {
+    "category": "people",
+    "moji": "🙂",
+    "unicodeVersion": "7.0",
     "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
   },
-  {
-    "name": "slightly_smiling_face",
-    "unicode": "1F642",
-    "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
-  },
-  {
-    "name": "slot_machine",
-    "unicode": "1F3B0",
+  "slot_machine": {
+    "category": "activity",
+    "moji": "🎰",
+    "unicodeVersion": "6.0",
     "digest": "914184788f8cd865cd074dca25c22acee31f5498117bd9a6e78cae67e6601652"
   },
-  {
-    "name": "small_blue_diamond",
-    "unicode": "1F539",
+  "small_blue_diamond": {
+    "category": "symbols",
+    "moji": "🔹",
+    "unicodeVersion": "6.0",
     "digest": "0b56d8e6b5ddf1f49fcc76e45e5fb2ee9f99ae6ffe682c26eaea4d9b7faac36c"
   },
-  {
-    "name": "small_orange_diamond",
-    "unicode": "1F538",
+  "small_orange_diamond": {
+    "category": "symbols",
+    "moji": "🔸",
+    "unicodeVersion": "6.0",
     "digest": "a2235830550e289c1608f2dcf5ede48f5c1a0eff45570699c39708c9677ab950"
   },
-  {
-    "name": "small_red_triangle",
-    "unicode": "1F53A",
+  "small_red_triangle": {
+    "category": "symbols",
+    "moji": "🔺",
+    "unicodeVersion": "6.0",
     "digest": "8c2985c4e9ce42d2f3b35539b879bc36206c5ef749f39fbd1eac51bd2676e1e5"
   },
-  {
-    "name": "small_red_triangle_down",
-    "unicode": "1F53B",
+  "small_red_triangle_down": {
+    "category": "symbols",
+    "moji": "🔻",
+    "unicodeVersion": "6.0",
     "digest": "46bd328df2fbf5d0597596bbf00d2d5f6e0c65bcb8f3fb325df8ba0c25e445b5"
   },
-  {
-    "name": "smile",
-    "unicode": "1F604",
+  "smile": {
+    "category": "people",
+    "moji": "😄",
+    "unicodeVersion": "6.0",
     "digest": "14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14"
   },
-  {
-    "name": "smile_cat",
-    "unicode": "1F638",
+  "smile_cat": {
+    "category": "people",
+    "moji": "😸",
+    "unicodeVersion": "6.0",
     "digest": "c35b76d6df100edb4022d762f47abfeb9f5e70886960c1d25908bd5d57ccb47e"
   },
-  {
-    "name": "smiley",
-    "unicode": "1F603",
+  "smiley": {
+    "category": "people",
+    "moji": "😃",
+    "unicodeVersion": "6.0",
     "digest": "a89f31eb9d814636852517a7f4eadec59195e2ac2cc9f8d124f1a1cc0f775b4a"
   },
-  {
-    "name": "smiley_cat",
-    "unicode": "1F63A",
+  "smiley_cat": {
+    "category": "people",
+    "moji": "😺",
+    "unicodeVersion": "6.0",
     "digest": "3e66a113c5e3e73fb94be29084cb27986b6bdb0e78ab44785bf2a35a550e71bf"
   },
-  {
-    "name": "smiling_imp",
-    "unicode": "1F608",
+  "smiling_imp": {
+    "category": "people",
+    "moji": "😈",
+    "unicodeVersion": "6.0",
     "digest": "3e02131d16525938f6facc7e097365dec7e13c8a0049a3be35fc29c80cc291b3"
   },
-  {
-    "name": "smirk",
-    "unicode": "1F60F",
+  "smirk": {
+    "category": "people",
+    "moji": "😏",
+    "unicodeVersion": "6.0",
     "digest": "3c180d46f5574d6fca3bb68eb02517da60b7008843cb3e90f2f9620d0c8ee943"
   },
-  {
-    "name": "smirk_cat",
-    "unicode": "1F63C",
+  "smirk_cat": {
+    "category": "people",
+    "moji": "😼",
+    "unicodeVersion": "6.0",
     "digest": "0683c7f73e1f65984e91313607d7cca21d99acd4b2e9932f00e0fffd0ce90742"
   },
-  {
-    "name": "smoking",
-    "unicode": "1F6AC",
+  "smoking": {
+    "category": "objects",
+    "moji": "🚬",
+    "unicodeVersion": "6.0",
     "digest": "baa9cb444bf0fe5c74358f981b19bc9e5c0415ced7f042baf93642282476ea61"
   },
-  {
-    "name": "snail",
-    "unicode": "1F40C",
+  "snail": {
+    "category": "nature",
+    "moji": "🐌",
+    "unicodeVersion": "6.0",
     "digest": "5733bf3672ae4b2b3e090fa670aeac70dcbcc04ca5b13abc8c8e53b8b3d4ff33"
   },
-  {
-    "name": "snake",
-    "unicode": "1F40D",
+  "snake": {
+    "category": "nature",
+    "moji": "🐍",
+    "unicodeVersion": "6.0",
     "digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773"
   },
-  {
-    "name": "sneezing_face",
-    "unicode": "1F927",
-    "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
-  },
-  {
-    "name": "sneeze",
-    "unicode": "1F927",
+  "sneezing_face": {
+    "category": "people",
+    "moji": "🤧",
+    "unicodeVersion": "9.0",
     "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
   },
-  {
-    "name": "snowboarder",
-    "unicode": "1F3C2",
+  "snowboarder": {
+    "category": "activity",
+    "moji": "🏂",
+    "unicodeVersion": "6.0",
     "digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19"
   },
-  {
-    "name": "snowflake",
-    "unicode": "2744",
+  "snowflake": {
+    "category": "nature",
+    "moji": "❄",
+    "unicodeVersion": "1.1",
     "digest": "6556c918e181df01ba849e76c43972d5310439971e5d8fc2409d112c05bf0028"
   },
-  {
-    "name": "snowman",
-    "unicode": "26C4",
+  "snowman": {
+    "category": "nature",
+    "moji": "⛄",
+    "unicodeVersion": "5.2",
     "digest": "6137456b2335e88e09c1859615eb22bb636355ef438f7a3949ad2f3d54478dd3"
   },
-  {
-    "name": "snowman2",
-    "unicode": "2603",
+  "snowman2": {
+    "category": "nature",
+    "moji": "☃",
+    "unicodeVersion": "1.1",
     "digest": "33ec75c22a13c81fa3c6b24a77ac1a08dc0dbe70b3716cf17b6702014d8a63fe"
   },
-  {
-    "name": "sob",
-    "unicode": "1F62D",
+  "sob": {
+    "category": "people",
+    "moji": "😭",
+    "unicodeVersion": "6.0",
     "digest": "d1ed4b31861f9f9fd4e9c95a9c17530e2320a1b4cad6ececb1545ce25d65e4ce"
   },
-  {
-    "name": "soccer",
-    "unicode": "26BD",
+  "soccer": {
+    "category": "activity",
+    "moji": "⚽",
+    "unicodeVersion": "5.2",
     "digest": "6a3f2e6a9a0b64c3fbf8705995792091daf386a4112dba75507a1f556f662f84"
   },
-  {
-    "name": "soon",
-    "unicode": "1F51C",
+  "soon": {
+    "category": "symbols",
+    "moji": "🔜",
+    "unicodeVersion": "6.0",
     "digest": "a49d1bcfbac3e6ccc05b9a9863eff74b0eb8b4d4b22b8b0f7b2787fcba1c73cc"
   },
-  {
-    "name": "sos",
-    "unicode": "1F198",
+  "sos": {
+    "category": "symbols",
+    "moji": "🆘",
+    "unicodeVersion": "6.0",
     "digest": "2fa7e0274383aeed6019eb9177e778d7aab8b88575b078b0ffeb77cd18df14b3"
   },
-  {
-    "name": "sound",
-    "unicode": "1F509",
+  "sound": {
+    "category": "symbols",
+    "moji": "🔉",
+    "unicodeVersion": "6.0",
     "digest": "faaca7b315b2495cbc381468580d25f1d11362441c35bb43d8a914f2ec8202d2"
   },
-  {
-    "name": "space_invader",
-    "unicode": "1F47E",
+  "space_invader": {
+    "category": "activity",
+    "moji": "👾",
+    "unicodeVersion": "6.0",
     "digest": "e75379cb5063f9a8861d762ad1886097c1697fbb61f2e4e8f531047955a4a2dd"
   },
-  {
-    "name": "spades",
-    "unicode": "2660",
+  "spades": {
+    "category": "symbols",
+    "moji": "♠",
+    "unicodeVersion": "1.1",
     "digest": "2c4d20f6a4893cfc62498d3f1f8f67577f39ed09f3e6682d8cb9cd8f365d30da"
   },
-  {
-    "name": "spaghetti",
-    "unicode": "1F35D",
+  "spaghetti": {
+    "category": "food",
+    "moji": "🍝",
+    "unicodeVersion": "6.0",
     "digest": "6d3451dc0faa1913539edb99261448f51735f269b61193c53dfe63466c0191e8"
   },
-  {
-    "name": "sparkle",
-    "unicode": "2747",
+  "sparkle": {
+    "category": "symbols",
+    "moji": "❇",
+    "unicodeVersion": "1.1",
     "digest": "7131163cd6c2f879110c86e9f068c33cf580f7c4b619449c41851fe6083402ee"
   },
-  {
-    "name": "sparkler",
-    "unicode": "1F387",
+  "sparkler": {
+    "category": "travel",
+    "moji": "🎇",
+    "unicodeVersion": "6.0",
     "digest": "88539ed8a13bd66e0c265c0913bd3ec2ddc4d95484323595713beb102221a1f6"
   },
-  {
-    "name": "sparkles",
-    "unicode": "2728",
+  "sparkles": {
+    "category": "nature",
+    "moji": "✨",
+    "unicodeVersion": "6.0",
     "digest": "cf84d16b1c0a381d5a7ae79031872747c9a6887eab6e92cc4a10a4b8600ef506"
   },
-  {
-    "name": "sparkling_heart",
-    "unicode": "1F496",
+  "sparkling_heart": {
+    "category": "symbols",
+    "moji": "💖",
+    "unicodeVersion": "6.0",
     "digest": "b80b1ddef83b6528b309a194f6f2faf5acab603daeb9254523efc2b941bcb6d2"
   },
-  {
-    "name": "speak_no_evil",
-    "unicode": "1F64A",
+  "speak_no_evil": {
+    "category": "nature",
+    "moji": "🙊",
+    "unicodeVersion": "6.0",
     "digest": "d2d7cfb4d471928a496bdc146890adc8422a68500b68115630b24c125d18e81f"
   },
-  {
-    "name": "speaker",
-    "unicode": "1F508",
+  "speaker": {
+    "category": "symbols",
+    "moji": "🔈",
+    "unicodeVersion": "6.0",
     "digest": "dbca5f7181728d2ad67ff76fd566ffbdf53e333e7eeed341f54668bd47969413"
   },
-  {
-    "name": "speaking_head",
-    "unicode": "1F5E3",
+  "speaking_head": {
+    "category": "people",
+    "moji": "🗣",
+    "unicodeVersion": "7.0",
     "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
   },
-  {
-    "name": "speaking_head_in_silhouette",
-    "unicode": "1F5E3",
-    "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
-  },
-  {
-    "name": "speech_balloon",
-    "unicode": "1F4AC",
+  "speech_balloon": {
+    "category": "symbols",
+    "moji": "💬",
+    "unicodeVersion": "6.0",
     "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
   },
-  {
-    "name": "speedboat",
-    "unicode": "1F6A4",
+  "speedboat": {
+    "category": "travel",
+    "moji": "🚤",
+    "unicodeVersion": "6.0",
     "digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576"
   },
-  {
-    "name": "spider",
-    "unicode": "1F577",
+  "spider": {
+    "category": "nature",
+    "moji": "🕷",
+    "unicodeVersion": "7.0",
     "digest": "8411eac0c1b80926fd93cc1d6423e00b05d04c485b79ee232da8f1714e899a37"
   },
-  {
-    "name": "spider_web",
-    "unicode": "1F578",
+  "spider_web": {
+    "category": "nature",
+    "moji": "🕸",
+    "unicodeVersion": "7.0",
     "digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23"
   },
-  {
-    "name": "spoon",
-    "unicode": "1F944",
+  "spoon": {
+    "category": "food",
+    "moji": "🥄",
+    "unicodeVersion": "9.0",
     "digest": "4fa31d59e5bffd2c45a8e01fcd5652e78a5691cbfa744e69882bc67173ddea05"
   },
-  {
-    "name": "spy",
-    "unicode": "1F575",
-    "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
-  },
-  {
-    "name": "sleuth_or_spy",
-    "unicode": "1F575",
+  "spy": {
+    "category": "people",
+    "moji": "🕵",
+    "unicodeVersion": "7.0",
     "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
   },
-  {
-    "name": "spy_tone1",
-    "unicode": "1F575-1F3FB",
+  "spy_tone1": {
+    "category": "people",
+    "moji": "🕵🏻",
+    "unicodeVersion": "8.0",
     "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
   },
-  {
-    "name": "sleuth_or_spy_tone1",
-    "unicode": "1F575-1F3FB",
-    "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
-  },
-  {
-    "name": "spy_tone2",
-    "unicode": "1F575-1F3FC",
-    "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
-  },
-  {
-    "name": "sleuth_or_spy_tone2",
-    "unicode": "1F575-1F3FC",
+  "spy_tone2": {
+    "category": "people",
+    "moji": "🕵🏼",
+    "unicodeVersion": "8.0",
     "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
   },
-  {
-    "name": "spy_tone3",
-    "unicode": "1F575-1F3FD",
+  "spy_tone3": {
+    "category": "people",
+    "moji": "🕵🏽",
+    "unicodeVersion": "8.0",
     "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
   },
-  {
-    "name": "sleuth_or_spy_tone3",
-    "unicode": "1F575-1F3FD",
-    "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
-  },
-  {
-    "name": "spy_tone4",
-    "unicode": "1F575-1F3FE",
-    "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
-  },
-  {
-    "name": "sleuth_or_spy_tone4",
-    "unicode": "1F575-1F3FE",
+  "spy_tone4": {
+    "category": "people",
+    "moji": "🕵🏾",
+    "unicodeVersion": "8.0",
     "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
   },
-  {
-    "name": "spy_tone5",
-    "unicode": "1F575-1F3FF",
+  "spy_tone5": {
+    "category": "people",
+    "moji": "🕵🏿",
+    "unicodeVersion": "8.0",
     "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
   },
-  {
-    "name": "sleuth_or_spy_tone5",
-    "unicode": "1F575-1F3FF",
-    "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
-  },
-  {
-    "name": "squid",
-    "unicode": "1F991",
+  "squid": {
+    "category": "nature",
+    "moji": "🦑",
+    "unicodeVersion": "9.0",
     "digest": "65a1b318c2c506b9d26cfd8282a5cf9922109595c8d12e92c3f7481ac7c08c49"
   },
-  {
-    "name": "stadium",
-    "unicode": "1F3DF",
+  "stadium": {
+    "category": "travel",
+    "moji": "🏟",
+    "unicodeVersion": "7.0",
     "digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f"
   },
-  {
-    "name": "star",
-    "unicode": "2B50",
+  "star": {
+    "category": "nature",
+    "moji": "⭐",
+    "unicodeVersion": "5.1",
     "digest": "d78e5c1b78caed103e100150c10b08a9ca3ee30c243943d6fc3cc08f422122e9"
   },
-  {
-    "name": "star2",
-    "unicode": "1F31F",
+  "star2": {
+    "category": "nature",
+    "moji": "🌟",
+    "unicodeVersion": "6.0",
     "digest": "f91ac4afe3f5d4a52847ae8b4a9704b591e00399aebba553d150d7e34ee939fa"
   },
-  {
-    "name": "star_and_crescent",
-    "unicode": "262A",
+  "star_and_crescent": {
+    "category": "symbols",
+    "moji": "☪",
+    "unicodeVersion": "1.1",
     "digest": "1bf3d29e50034f5e7c0dccff0a3a533b74bfa9b489e357b2739a473311f1332a"
   },
-  {
-    "name": "star_of_david",
-    "unicode": "2721",
+  "star_of_david": {
+    "category": "symbols",
+    "moji": "✡",
+    "unicodeVersion": "1.1",
     "digest": "28a0bd0eeac9d0835ceb8425d72c2472464e863dd09b76a0ddc1c08cf1986402"
   },
-  {
-    "name": "stars",
-    "unicode": "1F320",
+  "stars": {
+    "category": "travel",
+    "moji": "🌠",
+    "unicodeVersion": "6.0",
     "digest": "837d9045316b8fb5e533457eac61241534f641eb78d8cb75f688f80fb8e8a7f0"
   },
-  {
-    "name": "station",
-    "unicode": "1F689",
+  "station": {
+    "category": "travel",
+    "moji": "🚉",
+    "unicodeVersion": "6.0",
     "digest": "27a163ac0aea4ed247a121cae826eafc475977c68b0d888e9405bea14326ff56"
   },
-  {
-    "name": "statue_of_liberty",
-    "unicode": "1F5FD",
+  "statue_of_liberty": {
+    "category": "travel",
+    "moji": "🗽",
+    "unicodeVersion": "6.0",
     "digest": "f5a43599ab3f24ed3a78a745e06e2ac3e33107a292386ad81c67935ee5b22493"
   },
-  {
-    "name": "steam_locomotive",
-    "unicode": "1F682",
+  "steam_locomotive": {
+    "category": "travel",
+    "moji": "🚂",
+    "unicodeVersion": "6.0",
     "digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba"
   },
-  {
-    "name": "stew",
-    "unicode": "1F372",
+  "stew": {
+    "category": "food",
+    "moji": "🍲",
+    "unicodeVersion": "6.0",
     "digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df"
   },
-  {
-    "name": "stop_button",
-    "unicode": "23F9",
+  "stop_button": {
+    "category": "symbols",
+    "moji": "⏹",
+    "unicodeVersion": "7.0",
     "digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a"
   },
-  {
-    "name": "stopwatch",
-    "unicode": "23F1",
+  "stopwatch": {
+    "category": "objects",
+    "moji": "⏱",
+    "unicodeVersion": "6.0",
     "digest": "9b6b9491a24d8ab4f896eb876da7973f028bd5e7c51a3767ba7e61bb6fbb2be0"
   },
-  {
-    "name": "straight_ruler",
-    "unicode": "1F4CF",
+  "straight_ruler": {
+    "category": "objects",
+    "moji": "📏",
+    "unicodeVersion": "6.0",
     "digest": "cee31101767bd3f961363599924dc3790675d05a1285a8396428d2f91771c111"
   },
-  {
-    "name": "strawberry",
-    "unicode": "1F353",
+  "strawberry": {
+    "category": "food",
+    "moji": "🍓",
+    "unicodeVersion": "6.0",
     "digest": "5750a15e12f21259286ddbc3a8222a385b3b97a9f368897f42dd000060343174"
   },
-  {
-    "name": "stuck_out_tongue",
-    "unicode": "1F61B",
+  "stuck_out_tongue": {
+    "category": "people",
+    "moji": "😛",
+    "unicodeVersion": "6.1",
     "digest": "92dc42980a6dfdd7204fc874a762d6a0bbf0fdbfb5a7c0698fca04782e99fde6"
   },
-  {
-    "name": "stuck_out_tongue_closed_eyes",
-    "unicode": "1F61D",
+  "stuck_out_tongue_closed_eyes": {
+    "category": "people",
+    "moji": "😝",
+    "unicodeVersion": "6.0",
     "digest": "434d25ac24cad7ba699eae876a25d9a99b584449cca50b124bf6aa7f20a83d51"
   },
-  {
-    "name": "stuck_out_tongue_winking_eye",
-    "unicode": "1F61C",
+  "stuck_out_tongue_winking_eye": {
+    "category": "people",
+    "moji": "😜",
+    "unicodeVersion": "6.0",
     "digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d"
   },
-  {
-    "name": "stuffed_flatbread",
-    "unicode": "1F959",
-    "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
-  },
-  {
-    "name": "stuffed_pita",
-    "unicode": "1F959",
+  "stuffed_flatbread": {
+    "category": "food",
+    "moji": "🥙",
+    "unicodeVersion": "9.0",
     "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
   },
-  {
-    "name": "sun_with_face",
-    "unicode": "1F31E",
+  "sun_with_face": {
+    "category": "nature",
+    "moji": "🌞",
+    "unicodeVersion": "6.0",
     "digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be"
   },
-  {
-    "name": "sunflower",
-    "unicode": "1F33B",
+  "sunflower": {
+    "category": "nature",
+    "moji": "🌻",
+    "unicodeVersion": "6.0",
     "digest": "27d1161f50f932a6b26c404cf2e8f7083683ed0f2382d62b7472acccaa6eb695"
   },
-  {
-    "name": "sunglasses",
-    "unicode": "1F60E",
+  "sunglasses": {
+    "category": "people",
+    "moji": "😎",
+    "unicodeVersion": "6.0",
     "digest": "966684382e5c59e98319e4c0ea7c304c61c2638ad5408faa49ce2c83c4416757"
   },
-  {
-    "name": "sunny",
-    "unicode": "2600",
+  "sunny": {
+    "category": "nature",
+    "moji": "☀",
+    "unicodeVersion": "1.1",
     "digest": "460fea4cbbdd1595450c1033a2ee5de7fea2e2f147822efa49f7e204812415aa"
   },
-  {
-    "name": "sunrise",
-    "unicode": "1F305",
+  "sunrise": {
+    "category": "travel",
+    "moji": "🌅",
+    "unicodeVersion": "6.0",
     "digest": "7718a49636b0cdd1862ed67c7a9d6e72f471c2591ff0d912485b1be55d1ea115"
   },
-  {
-    "name": "sunrise_over_mountains",
-    "unicode": "1F304",
+  "sunrise_over_mountains": {
+    "category": "travel",
+    "moji": "🌄",
+    "unicodeVersion": "6.0",
     "digest": "743d0701cdbe2a814962363813c3153d3c5e62c3e410349f56d49dbb9581f356"
   },
-  {
-    "name": "surfer",
-    "unicode": "1F3C4",
+  "surfer": {
+    "category": "activity",
+    "moji": "🏄",
+    "unicodeVersion": "6.0",
     "digest": "bb440775e9213430942015c37db8de58b5a561ee971b2a0f3993fc3f1d2554d4"
   },
-  {
-    "name": "surfer_tone1",
-    "unicode": "1F3C4-1F3FB",
+  "surfer_tone1": {
+    "category": "activity",
+    "moji": "🏄🏻",
+    "unicodeVersion": "8.0",
     "digest": "a4937b030aca30b68bb644f37cf63c38aebce3c00b57d1c8a0ffe596b57d2f1e"
   },
-  {
-    "name": "surfer_tone2",
-    "unicode": "1F3C4-1F3FC",
+  "surfer_tone2": {
+    "category": "activity",
+    "moji": "🏄🏼",
+    "unicodeVersion": "8.0",
     "digest": "1c2a954a9c5284dedf0327d6f3c954c9fdd3953b848076d298874775ad8bf0a3"
   },
-  {
-    "name": "surfer_tone3",
-    "unicode": "1F3C4-1F3FD",
+  "surfer_tone3": {
+    "category": "activity",
+    "moji": "🏄🏽",
+    "unicodeVersion": "8.0",
     "digest": "418a3408b9ab026124f067c8597b500217e56bc28d9844a29eea5eee6f604ff8"
   },
-  {
-    "name": "surfer_tone4",
-    "unicode": "1F3C4-1F3FE",
+  "surfer_tone4": {
+    "category": "activity",
+    "moji": "🏄🏾",
+    "unicodeVersion": "8.0",
     "digest": "530870b9ac9f4d45ff750e264feb90b44fb93ca2852f323987b06f5f12fb5a4d"
   },
-  {
-    "name": "surfer_tone5",
-    "unicode": "1F3C4-1F3FF",
+  "surfer_tone5": {
+    "category": "activity",
+    "moji": "🏄🏿",
+    "unicodeVersion": "8.0",
     "digest": "40e11b1ae652cfd085d083377f1da24160065ed1b67403c6fa4655e6e44169ec"
   },
-  {
-    "name": "sushi",
-    "unicode": "1F363",
+  "sushi": {
+    "category": "food",
+    "moji": "🍣",
+    "unicodeVersion": "6.0",
     "digest": "b924c621236ca3284b349b0509ae1043f2fc2c7f6d67615716f9717ada78c992"
   },
-  {
-    "name": "suspension_railway",
-    "unicode": "1F69F",
+  "suspension_railway": {
+    "category": "travel",
+    "moji": "🚟",
+    "unicodeVersion": "6.0",
     "digest": "cd3d21da79864f0c018b863e82fb0561fff3c5e3c065303cfcb89c3663d638ba"
   },
-  {
-    "name": "sweat",
-    "unicode": "1F613",
+  "sweat": {
+    "category": "people",
+    "moji": "😓",
+    "unicodeVersion": "6.0",
     "digest": "1aa771479aa1ac5eeea4bafbe93ebd85a0f692f6d869034f31e25b689c2e264d"
   },
-  {
-    "name": "sweat_drops",
-    "unicode": "1F4A6",
+  "sweat_drops": {
+    "category": "nature",
+    "moji": "💦",
+    "unicodeVersion": "6.0",
     "digest": "b575b85415bc9852cf6415d417ebf799167fde03c6819ebcaa24ae1b3dde8dab"
   },
-  {
-    "name": "sweat_smile",
-    "unicode": "1F605",
+  "sweat_smile": {
+    "category": "people",
+    "moji": "😅",
+    "unicodeVersion": "6.0",
     "digest": "171b0d0845d46c33bedb6d3b39fb1ff366e22ba90685eedabebd91bb2b0680de"
   },
-  {
-    "name": "sweet_potato",
-    "unicode": "1F360",
+  "sweet_potato": {
+    "category": "food",
+    "moji": "🍠",
+    "unicodeVersion": "6.0",
     "digest": "4b91920f0b87d42763313bc476f4c821a74e4c12dc1c92165a859dddeaaf8844"
   },
-  {
-    "name": "swimmer",
-    "unicode": "1F3CA",
+  "swimmer": {
+    "category": "activity",
+    "moji": "🏊",
+    "unicodeVersion": "6.0",
     "digest": "2c4ed4a51aad99d9957ae11a219d5164db9748fc3a65002c6085a9f15adfa9e2"
   },
-  {
-    "name": "swimmer_tone1",
-    "unicode": "1F3CA-1F3FB",
+  "swimmer_tone1": {
+    "category": "activity",
+    "moji": "🏊🏻",
+    "unicodeVersion": "8.0",
     "digest": "48588f129ee4af52ca2e0f4594213391978601087cd607896b2f979ca077284b"
   },
-  {
-    "name": "swimmer_tone2",
-    "unicode": "1F3CA-1F3FC",
+  "swimmer_tone2": {
+    "category": "activity",
+    "moji": "🏊🏼",
+    "unicodeVersion": "8.0",
     "digest": "fff209448524bd1ef4d6decabf6c1ead94c8d3d5b1bfb5e54f20cc8e139232fc"
   },
-  {
-    "name": "swimmer_tone3",
-    "unicode": "1F3CA-1F3FD",
+  "swimmer_tone3": {
+    "category": "activity",
+    "moji": "🏊🏽",
+    "unicodeVersion": "8.0",
     "digest": "2003932cb2cf4ae9a10b23338bf375a9293fb18c0ecf91bdfae73be6eebb3800"
   },
-  {
-    "name": "swimmer_tone4",
-    "unicode": "1F3CA-1F3FE",
+  "swimmer_tone4": {
+    "category": "activity",
+    "moji": "🏊🏾",
+    "unicodeVersion": "8.0",
     "digest": "20b4bff9baa1c694ad98067dde834c56092f023b9664bec382c2e512232bd480"
   },
-  {
-    "name": "swimmer_tone5",
-    "unicode": "1F3CA-1F3FF",
+  "swimmer_tone5": {
+    "category": "activity",
+    "moji": "🏊🏿",
+    "unicodeVersion": "8.0",
     "digest": "0ff8eb57c2be8e80a1bc6ba75b8d9ffb9bd8d3be636150c4c03399ec1886f218"
   },
-  {
-    "name": "symbols",
-    "unicode": "1F523",
+  "symbols": {
+    "category": "symbols",
+    "moji": "🔣",
+    "unicodeVersion": "6.0",
     "digest": "2a2a79816c4d0751a0d73586eec5e63b410653d3c85cc968906bf1fc03d89b94"
   },
-  {
-    "name": "synagogue",
-    "unicode": "1F54D",
+  "synagogue": {
+    "category": "travel",
+    "moji": "🕍",
+    "unicodeVersion": "8.0",
     "digest": "98569cdd7c61528963b67b7891dfa46025c5e810cbb22ee18ddb3bd85de2da69"
   },
-  {
-    "name": "syringe",
-    "unicode": "1F489",
+  "syringe": {
+    "category": "objects",
+    "moji": "💉",
+    "unicodeVersion": "6.0",
     "digest": "e1538e645ccc571227c994b71b3d1be2c4d072d8bd9c944a42ff4a11c91a34a6"
   },
-  {
-    "name": "taco",
-    "unicode": "1F32E",
+  "taco": {
+    "category": "food",
+    "moji": "🌮",
+    "unicodeVersion": "8.0",
     "digest": "e1e45aefdb7445faeae75c3831df6a3d6f2590fcdd48a20d847593c246df613b"
   },
-  {
-    "name": "tada",
-    "unicode": "1F389",
+  "tada": {
+    "category": "objects",
+    "moji": "🎉",
+    "unicodeVersion": "6.0",
     "digest": "1d2e6cbb2a3244240bc70209715d2213d1efee2e370cccfbcc046c333ae2d650"
   },
-  {
-    "name": "tanabata_tree",
-    "unicode": "1F38B",
+  "tanabata_tree": {
+    "category": "nature",
+    "moji": "🎋",
+    "unicodeVersion": "6.0",
     "digest": "592f2907ffc1b914390e1a106c15120ff3607e99192158b94d237975647c5540"
   },
-  {
-    "name": "tangerine",
-    "unicode": "1F34A",
+  "tangerine": {
+    "category": "food",
+    "moji": "🍊",
+    "unicodeVersion": "6.0",
     "digest": "40c9ddcde1b0bcfaeb466629a87825eb8c2037835720cbee5e2fda04be3c8d0a"
   },
-  {
-    "name": "taurus",
-    "unicode": "2649",
+  "taurus": {
+    "category": "symbols",
+    "moji": "♉",
+    "unicodeVersion": "1.1",
     "digest": "21cf24cb6410ab6596e2df8b3e242cc07f9dbb247eabc00c590fe184b373d068"
   },
-  {
-    "name": "taxi",
-    "unicode": "1F695",
+  "taxi": {
+    "category": "travel",
+    "moji": "🚕",
+    "unicodeVersion": "6.0",
     "digest": "c546cc743831cfbf0c15452767cf2a4faf3775066797e997ae7c1fcbe4eca479"
   },
-  {
-    "name": "tea",
-    "unicode": "1F375",
+  "tea": {
+    "category": "food",
+    "moji": "🍵",
+    "unicodeVersion": "6.0",
     "digest": "00e3f1e389fa58c4fcd8c53ebbf83d25872f4315845ab1984b35410ae65553d9"
   },
-  {
-    "name": "telephone",
-    "unicode": "260E",
+  "telephone": {
+    "category": "objects",
+    "moji": "☎",
+    "unicodeVersion": "1.1",
     "digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7"
   },
-  {
-    "name": "telephone_receiver",
-    "unicode": "1F4DE",
+  "telephone_receiver": {
+    "category": "objects",
+    "moji": "📞",
+    "unicodeVersion": "6.0",
     "digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046"
   },
-  {
-    "name": "telescope",
-    "unicode": "1F52D",
+  "telescope": {
+    "category": "objects",
+    "moji": "🔭",
+    "unicodeVersion": "6.0",
     "digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495"
   },
-  {
-    "name": "ten",
-    "unicode": "1F51F",
+  "ten": {
+    "category": "symbols",
+    "moji": "🔟",
+    "unicodeVersion": "6.0",
     "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40"
   },
-  {
-    "name": "tennis",
-    "unicode": "1F3BE",
+  "tennis": {
+    "category": "activity",
+    "moji": "🎾",
+    "unicodeVersion": "6.0",
     "digest": "dc1600b4d8dce3d26259eb0d1c6ab042566565e3c1f2c96112210f1550a716fd"
   },
-  {
-    "name": "tent",
-    "unicode": "26FA",
+  "tent": {
+    "category": "travel",
+    "moji": "⛺",
+    "unicodeVersion": "5.2",
     "digest": "30d9b17ac3219d4970ddf54d7c1a288b0ae50f7f3b82ed232c0b1b19ef585662"
   },
-  {
-    "name": "thermometer",
-    "unicode": "1F321",
+  "thermometer": {
+    "category": "objects",
+    "moji": "🌡",
+    "unicodeVersion": "7.0",
     "digest": "66616babbcaef256d7b652796c760e8e893cb950c073348a408fe70904f80f25"
   },
-  {
-    "name": "thermometer_face",
-    "unicode": "1F912",
+  "thermometer_face": {
+    "category": "people",
+    "moji": "🤒",
+    "unicodeVersion": "8.0",
     "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
   },
-  {
-    "name": "face_with_thermometer",
-    "unicode": "1F912",
-    "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
-  },
-  {
-    "name": "thinking",
-    "unicode": "1F914",
-    "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
-  },
-  {
-    "name": "thinking_face",
-    "unicode": "1F914",
+  "thinking": {
+    "category": "people",
+    "moji": "🤔",
+    "unicodeVersion": "8.0",
     "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
   },
-  {
-    "name": "third_place",
-    "unicode": "1F949",
+  "third_place": {
+    "category": "activity",
+    "moji": "🥉",
+    "unicodeVersion": "9.0",
     "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
   },
-  {
-    "name": "third_place_medal",
-    "unicode": "1F949",
-    "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
-  },
-  {
-    "name": "thought_balloon",
-    "unicode": "1F4AD",
+  "thought_balloon": {
+    "category": "symbols",
+    "moji": "💭",
+    "unicodeVersion": "6.0",
     "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e"
   },
-  {
-    "name": "three",
-    "unicode": "0033-20E3",
+  "three": {
+    "category": "symbols",
+    "moji": "3️⃣",
+    "unicodeVersion": "3.0",
     "digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6"
   },
-  {
-    "name": "thumbsdown",
-    "unicode": "1F44E",
-    "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
-  },
-  {
-    "name": "-1",
-    "unicode": "1F44E",
+  "thumbsdown": {
+    "category": "people",
+    "moji": "👎",
+    "unicodeVersion": "6.0",
     "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
   },
-  {
-    "name": "thumbsdown_tone1",
-    "unicode": "1F44E-1F3FB",
+  "thumbsdown_tone1": {
+    "category": "people",
+    "moji": "👎🏻",
+    "unicodeVersion": "8.0",
     "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
   },
-  {
-    "name": "-1_tone1",
-    "unicode": "1F44E-1F3FB",
-    "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
-  },
-  {
-    "name": "thumbsdown_tone2",
-    "unicode": "1F44E-1F3FC",
-    "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
-  },
-  {
-    "name": "-1_tone2",
-    "unicode": "1F44E-1F3FC",
+  "thumbsdown_tone2": {
+    "category": "people",
+    "moji": "👎🏼",
+    "unicodeVersion": "8.0",
     "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
   },
-  {
-    "name": "thumbsdown_tone3",
-    "unicode": "1F44E-1F3FD",
+  "thumbsdown_tone3": {
+    "category": "people",
+    "moji": "👎🏽",
+    "unicodeVersion": "8.0",
     "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
   },
-  {
-    "name": "-1_tone3",
-    "unicode": "1F44E-1F3FD",
-    "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
-  },
-  {
-    "name": "thumbsdown_tone4",
-    "unicode": "1F44E-1F3FE",
-    "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
-  },
-  {
-    "name": "-1_tone4",
-    "unicode": "1F44E-1F3FE",
+  "thumbsdown_tone4": {
+    "category": "people",
+    "moji": "👎🏾",
+    "unicodeVersion": "8.0",
     "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
   },
-  {
-    "name": "thumbsdown_tone5",
-    "unicode": "1F44E-1F3FF",
+  "thumbsdown_tone5": {
+    "category": "people",
+    "moji": "👎🏿",
+    "unicodeVersion": "8.0",
     "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
   },
-  {
-    "name": "-1_tone5",
-    "unicode": "1F44E-1F3FF",
-    "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
-  },
-  {
-    "name": "thumbsup",
-    "unicode": "1F44D",
-    "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
-  },
-  {
-    "name": "+1",
-    "unicode": "1F44D",
+  "thumbsup": {
+    "category": "people",
+    "moji": "👍",
+    "unicodeVersion": "6.0",
     "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
   },
-  {
-    "name": "thumbsup_tone1",
-    "unicode": "1F44D-1F3FB",
+  "thumbsup_tone1": {
+    "category": "people",
+    "moji": "👍🏻",
+    "unicodeVersion": "8.0",
     "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
   },
-  {
-    "name": "+1_tone1",
-    "unicode": "1F44D-1F3FB",
-    "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
-  },
-  {
-    "name": "thumbsup_tone2",
-    "unicode": "1F44D-1F3FC",
-    "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
-  },
-  {
-    "name": "+1_tone2",
-    "unicode": "1F44D-1F3FC",
+  "thumbsup_tone2": {
+    "category": "people",
+    "moji": "👍🏼",
+    "unicodeVersion": "8.0",
     "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
   },
-  {
-    "name": "thumbsup_tone3",
-    "unicode": "1F44D-1F3FD",
+  "thumbsup_tone3": {
+    "category": "people",
+    "moji": "👍🏽",
+    "unicodeVersion": "8.0",
     "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
   },
-  {
-    "name": "+1_tone3",
-    "unicode": "1F44D-1F3FD",
-    "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
-  },
-  {
-    "name": "thumbsup_tone4",
-    "unicode": "1F44D-1F3FE",
-    "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
-  },
-  {
-    "name": "+1_tone4",
-    "unicode": "1F44D-1F3FE",
+  "thumbsup_tone4": {
+    "category": "people",
+    "moji": "👍🏾",
+    "unicodeVersion": "8.0",
     "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
   },
-  {
-    "name": "thumbsup_tone5",
-    "unicode": "1F44D-1F3FF",
+  "thumbsup_tone5": {
+    "category": "people",
+    "moji": "👍🏿",
+    "unicodeVersion": "8.0",
     "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
   },
-  {
-    "name": "+1_tone5",
-    "unicode": "1F44D-1F3FF",
-    "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
-  },
-  {
-    "name": "thunder_cloud_rain",
-    "unicode": "26C8",
-    "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
-  },
-  {
-    "name": "thunder_cloud_and_rain",
-    "unicode": "26C8",
+  "thunder_cloud_rain": {
+    "category": "nature",
+    "moji": "⛈",
+    "unicodeVersion": "5.2",
     "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
   },
-  {
-    "name": "ticket",
-    "unicode": "1F3AB",
+  "ticket": {
+    "category": "activity",
+    "moji": "🎫",
+    "unicodeVersion": "6.0",
     "digest": "b4326fe7761940216e6c76ee2928110a6b37bf913da9d694e96557e7c7c10420"
   },
-  {
-    "name": "tickets",
-    "unicode": "1F39F",
+  "tickets": {
+    "category": "activity",
+    "moji": "🎟",
+    "unicodeVersion": "7.0",
     "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
   },
-  {
-    "name": "admission_tickets",
-    "unicode": "1F39F",
-    "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
-  },
-  {
-    "name": "tiger",
-    "unicode": "1F42F",
+  "tiger": {
+    "category": "nature",
+    "moji": "🐯",
+    "unicodeVersion": "6.0",
     "digest": "e139531e6c930bc46242dc0ed274661229de026b5419d8ea8f99fdb0f8a719ab"
   },
-  {
-    "name": "tiger2",
-    "unicode": "1F405",
+  "tiger2": {
+    "category": "nature",
+    "moji": "🐅",
+    "unicodeVersion": "6.0",
     "digest": "f930cc8714198310d9b0edca6baff243ac5a3320f75fadb56fa5acc6fe34ff24"
   },
-  {
-    "name": "timer",
-    "unicode": "23F2",
-    "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
-  },
-  {
-    "name": "timer_clock",
-    "unicode": "23F2",
+  "timer": {
+    "category": "objects",
+    "moji": "⏲",
+    "unicodeVersion": "6.0",
     "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
   },
-  {
-    "name": "tired_face",
-    "unicode": "1F62B",
+  "tired_face": {
+    "category": "people",
+    "moji": "😫",
+    "unicodeVersion": "6.0",
     "digest": "775739bc9324517e614878ca0960d793df97775feeb62b14dbfb311a42a21802"
   },
-  {
-    "name": "tm",
-    "unicode": "2122",
+  "tm": {
+    "category": "symbols",
+    "moji": "™",
+    "unicodeVersion": "1.1",
     "digest": "7d9fafdb72d91860478fc185719f289f359eab2c368a132cb936a269e2ab6a24"
   },
-  {
-    "name": "toilet",
-    "unicode": "1F6BD",
+  "toilet": {
+    "category": "objects",
+    "moji": "🚽",
+    "unicodeVersion": "6.0",
     "digest": "0d1b0dd0078f51104e8632a0726e1b3f075561a1ffa8a2546602de15798415d0"
   },
-  {
-    "name": "tokyo_tower",
-    "unicode": "1F5FC",
+  "tokyo_tower": {
+    "category": "travel",
+    "moji": "🗼",
+    "unicodeVersion": "6.0",
     "digest": "73eaf6fd59d16396673afef620c6d928857d5cf616e95a40eaf2861686e0956a"
   },
-  {
-    "name": "tomato",
-    "unicode": "1F345",
+  "tomato": {
+    "category": "food",
+    "moji": "🍅",
+    "unicodeVersion": "6.0",
     "digest": "d092d8ad381d542e59b6a82b4f1ef0d10fc1ed48460952375c6c5c6258cea111"
   },
-  {
-    "name": "tone1",
-    "unicode": "1F3FB",
+  "tone1": {
+    "category": "modifier",
+    "moji": "🏻",
+    "unicodeVersion": "8.0",
     "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c"
   },
-  {
-    "name": "tone2",
-    "unicode": "1F3FC",
+  "tone2": {
+    "category": "modifier",
+    "moji": "🏼",
+    "unicodeVersion": "8.0",
     "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f"
   },
-  {
-    "name": "tone3",
-    "unicode": "1F3FD",
+  "tone3": {
+    "category": "modifier",
+    "moji": "🏽",
+    "unicodeVersion": "8.0",
     "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8"
   },
-  {
-    "name": "tone4",
-    "unicode": "1F3FE",
+  "tone4": {
+    "category": "modifier",
+    "moji": "🏾",
+    "unicodeVersion": "8.0",
     "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3"
   },
-  {
-    "name": "tone5",
-    "unicode": "1F3FF",
+  "tone5": {
+    "category": "modifier",
+    "moji": "🏿",
+    "unicodeVersion": "8.0",
     "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81"
   },
-  {
-    "name": "tongue",
-    "unicode": "1F445",
+  "tongue": {
+    "category": "people",
+    "moji": "👅",
+    "unicodeVersion": "6.0",
     "digest": "286e9d2583c371431d6fc979dd4ab48981676da26baada51a846657a3654c19b"
   },
-  {
-    "name": "tools",
-    "unicode": "1F6E0",
+  "tools": {
+    "category": "objects",
+    "moji": "🛠",
+    "unicodeVersion": "7.0",
     "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
   },
-  {
-    "name": "hammer_and_wrench",
-    "unicode": "1F6E0",
-    "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
-  },
-  {
-    "name": "top",
-    "unicode": "1F51D",
+  "top": {
+    "category": "symbols",
+    "moji": "🔝",
+    "unicodeVersion": "6.0",
     "digest": "c9a9f25b17db014e76b6be54aa07ef89bb18f8adb41b3199d180a559ff1d9ea5"
   },
-  {
-    "name": "tophat",
-    "unicode": "1F3A9",
+  "tophat": {
+    "category": "people",
+    "moji": "🎩",
+    "unicodeVersion": "6.0",
     "digest": "43a45dfb5d6b57a63a0491f4e3ec780774c0301b53ed39a303a0bd803d16ed71"
   },
-  {
-    "name": "track_next",
-    "unicode": "23ED",
-    "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
-  },
-  {
-    "name": "next_track",
-    "unicode": "23ED",
+  "track_next": {
+    "category": "symbols",
+    "moji": "⏭",
+    "unicodeVersion": "6.0",
     "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
   },
-  {
-    "name": "track_previous",
-    "unicode": "23EE",
+  "track_previous": {
+    "category": "symbols",
+    "moji": "⏮",
+    "unicodeVersion": "6.0",
     "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
   },
-  {
-    "name": "previous_track",
-    "unicode": "23EE",
-    "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
-  },
-  {
-    "name": "trackball",
-    "unicode": "1F5B2",
+  "trackball": {
+    "category": "objects",
+    "moji": "🖲",
+    "unicodeVersion": "7.0",
     "digest": "32a819a3129429f797ad434d0c40e263dc236808e34878c599ed2304b43702f5"
   },
-  {
-    "name": "tractor",
-    "unicode": "1F69C",
+  "tractor": {
+    "category": "travel",
+    "moji": "🚜",
+    "unicodeVersion": "6.0",
     "digest": "5e4686290f1a4c9953ae208340b7d276f25b3b2197a43e52469aeb6450e93997"
   },
-  {
-    "name": "traffic_light",
-    "unicode": "1F6A5",
+  "traffic_light": {
+    "category": "travel",
+    "moji": "🚥",
+    "unicodeVersion": "6.0",
     "digest": "d96aacade33d1ad3e0414f8a920513010f36eb7e5889774251c1d91148917ead"
   },
-  {
-    "name": "train",
-    "unicode": "1F68B",
+  "train": {
+    "category": "travel",
+    "moji": "🚋",
+    "unicodeVersion": "6.0",
     "digest": "7423d17e131df7aadaa350b5d39dcbce3b28de331ff8b6703a3b2d0093963f4b"
   },
-  {
-    "name": "train2",
-    "unicode": "1F686",
+  "train2": {
+    "category": "travel",
+    "moji": "🚆",
+    "unicodeVersion": "6.0",
     "digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122"
   },
-  {
-    "name": "tram",
-    "unicode": "1F68A",
+  "tram": {
+    "category": "travel",
+    "moji": "🚊",
+    "unicodeVersion": "6.0",
     "digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100"
   },
-  {
-    "name": "triangular_flag_on_post",
-    "unicode": "1F6A9",
+  "triangular_flag_on_post": {
+    "category": "objects",
+    "moji": "🚩",
+    "unicodeVersion": "6.0",
     "digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da"
   },
-  {
-    "name": "triangular_ruler",
-    "unicode": "1F4D0",
+  "triangular_ruler": {
+    "category": "objects",
+    "moji": "📐",
+    "unicodeVersion": "6.0",
     "digest": "a0367dcf663ec934f1fc7c88bfaccc02b229a896f60930a66bb02241c933e501"
   },
-  {
-    "name": "trident",
-    "unicode": "1F531",
+  "trident": {
+    "category": "symbols",
+    "moji": "🔱",
+    "unicodeVersion": "6.0",
     "digest": "ee45920845d3b35c2e45b934cf30ce97bfe2f24c5d72ef1ac6e0842e52b50fc1"
   },
-  {
-    "name": "triumph",
-    "unicode": "1F624",
+  "triumph": {
+    "category": "people",
+    "moji": "😤",
+    "unicodeVersion": "6.0",
     "digest": "4aa44b8e1682c1269624a359f4b0bf613553683b883d947561ab169d7f85da0f"
   },
-  {
-    "name": "trolleybus",
-    "unicode": "1F68E",
+  "trolleybus": {
+    "category": "travel",
+    "moji": "🚎",
+    "unicodeVersion": "6.0",
     "digest": "f610b4fd1123f06778a8e3bb8f738d5b0079aeb0b0926b6a63268c0dd0ee03ed"
   },
-  {
-    "name": "trophy",
-    "unicode": "1F3C6",
+  "trophy": {
+    "category": "activity",
+    "moji": "🏆",
+    "unicodeVersion": "6.0",
     "digest": "50cfbedac18bf0fa5dec727643e15ec47f64068944b536e97518ee3be4f08006"
   },
-  {
-    "name": "tropical_drink",
-    "unicode": "1F379",
+  "tropical_drink": {
+    "category": "food",
+    "moji": "🍹",
+    "unicodeVersion": "6.0",
     "digest": "54144fce60d650f426b1edf09e47c70b2762222398c1fe40231881f074603a69"
   },
-  {
-    "name": "tropical_fish",
-    "unicode": "1F420",
+  "tropical_fish": {
+    "category": "nature",
+    "moji": "🐠",
+    "unicodeVersion": "6.0",
     "digest": "fd92100aaa9328da35e6090388824921b9726b474d1432a926d2cf9c45ad6528"
   },
-  {
-    "name": "truck",
-    "unicode": "1F69A",
+  "truck": {
+    "category": "travel",
+    "moji": "🚚",
+    "unicodeVersion": "6.0",
     "digest": "0d1571e58e900abc453df0ff683fe7acb5906ecbdd52ab35b7101074359faf18"
   },
-  {
-    "name": "trumpet",
-    "unicode": "1F3BA",
+  "trumpet": {
+    "category": "activity",
+    "moji": "🎺",
+    "unicodeVersion": "6.0",
     "digest": "cea3614c309f5573f328f4603120dbe930016a35f0dfa400b0d968fe9fff2d55"
   },
-  {
-    "name": "tulip",
-    "unicode": "1F337",
+  "tulip": {
+    "category": "nature",
+    "moji": "🌷",
+    "unicodeVersion": "6.0",
     "digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086"
   },
-  {
-    "name": "tumbler_glass",
-    "unicode": "1F943",
-    "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
-  },
-  {
-    "name": "whisky",
-    "unicode": "1F943",
+  "tumbler_glass": {
+    "category": "food",
+    "moji": "🥃",
+    "unicodeVersion": "9.0",
     "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
   },
-  {
-    "name": "turkey",
-    "unicode": "1F983",
+  "turkey": {
+    "category": "nature",
+    "moji": "🦃",
+    "unicodeVersion": "8.0",
     "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4"
   },
-  {
-    "name": "turtle",
-    "unicode": "1F422",
+  "turtle": {
+    "category": "nature",
+    "moji": "🐢",
+    "unicodeVersion": "6.0",
     "digest": "588c35fb42c9502a908e9805517d4cc8c4ba4e74c9beed4035779fea1efe14f8"
   },
-  {
-    "name": "tv",
-    "unicode": "1F4FA",
+  "tv": {
+    "category": "objects",
+    "moji": "📺",
+    "unicodeVersion": "6.0",
     "digest": "1279f3f3955a58dbbf74e248fc914b0bdba9c4c6b6a5176e9d12bf2750ecfeb4"
   },
-  {
-    "name": "twisted_rightwards_arrows",
-    "unicode": "1F500",
+  "twisted_rightwards_arrows": {
+    "category": "symbols",
+    "moji": "🔀",
+    "unicodeVersion": "6.0",
     "digest": "fed07eebc2cf0d977ca0826bbd80defafbbcf118508444148f47b58949ebe27c"
   },
-  {
-    "name": "two",
-    "unicode": "0032-20E3",
+  "two": {
+    "category": "symbols",
+    "moji": "2️⃣",
+    "unicodeVersion": "3.0",
     "digest": "b346f51f6523b02ebcbd753256804e2f9cc1574c96aa634362bf9401dac2c661"
   },
-  {
-    "name": "two_hearts",
-    "unicode": "1F495",
+  "two_hearts": {
+    "category": "symbols",
+    "moji": "💕",
+    "unicodeVersion": "6.0",
     "digest": "6ded120a59aed790b441ec8fbbdea6f5cbfb4fa48e9e4b224cc29c9fde2d2e4c"
   },
-  {
-    "name": "two_men_holding_hands",
-    "unicode": "1F46C",
+  "two_men_holding_hands": {
+    "category": "people",
+    "moji": "👬",
+    "unicodeVersion": "6.0",
     "digest": "bfcf9e20a67d00262cdf6e85f1acd545dda91f2e370d68bfd41ce02f232a2987"
   },
-  {
-    "name": "two_women_holding_hands",
-    "unicode": "1F46D",
+  "two_women_holding_hands": {
+    "category": "people",
+    "moji": "👭",
+    "unicodeVersion": "6.0",
     "digest": "9d9d2b37a7f8e16fde1468dd8b5645003ea81ae4bf8bcf68471e2381845dd0dd"
   },
-  {
-    "name": "u5272",
-    "unicode": "1F239",
+  "u5272": {
+    "category": "symbols",
+    "moji": "🈹",
+    "unicodeVersion": "6.0",
     "digest": "01e6cb8f74ea3c19fdade59c2d13d158b90dc6b4b293421b2014b7478bf20870"
   },
-  {
-    "name": "u5408",
-    "unicode": "1F234",
+  "u5408": {
+    "category": "symbols",
+    "moji": "🈴",
+    "unicodeVersion": "6.0",
     "digest": "084cdbd5436670ea4dc22010e269c1ab7b0432897b8675301e69120374bcdd14"
   },
-  {
-    "name": "u55b6",
-    "unicode": "1F23A",
+  "u55b6": {
+    "category": "symbols",
+    "moji": "🈺",
+    "unicodeVersion": "6.0",
     "digest": "c1017023d20d4aae78d59342dd3bfc5282716ea0601d9a8c2476335cbf7a2e12"
   },
-  {
-    "name": "u6307",
-    "unicode": "1F22F",
+  "u6307": {
+    "category": "symbols",
+    "moji": "🈯",
+    "unicodeVersion": "5.2",
     "digest": "f459b092b974f459db1fb9cc13617a448b2e4f2b4dc46cc316d8c46af6e7d8bd"
   },
-  {
-    "name": "u6708",
-    "unicode": "1F237",
+  "u6708": {
+    "category": "symbols",
+    "moji": "🈷",
+    "unicodeVersion": "6.0",
     "digest": "928815abf5b30f92efe5168de0c7e6cf8c17899a03e358ab42f42667e0a4a04c"
   },
-  {
-    "name": "u6709",
-    "unicode": "1F236",
+  "u6709": {
+    "category": "symbols",
+    "moji": "🈶",
+    "unicodeVersion": "6.0",
     "digest": "f63a48ee06c892d24acec8b5634c021658d2ebde67a42d8faa86f27804a9f26d"
   },
-  {
-    "name": "u6e80",
-    "unicode": "1F235",
+  "u6e80": {
+    "category": "symbols",
+    "moji": "🈵",
+    "unicodeVersion": "6.0",
     "digest": "489181d90a5e43068459530673a153e4af04fdad8514ec341ff7afbcfd366c3b"
   },
-  {
-    "name": "u7121",
-    "unicode": "1F21A",
+  "u7121": {
+    "category": "symbols",
+    "moji": "🈚",
+    "unicodeVersion": "5.2",
     "digest": "9c50fd2ba14221affd2dcd3746322c2137dd75458493f4d385b544eb5bd8d6cd"
   },
-  {
-    "name": "u7533",
-    "unicode": "1F238",
+  "u7533": {
+    "category": "symbols",
+    "moji": "🈸",
+    "unicodeVersion": "6.0",
     "digest": "2b05819b380a2ea47cc5fde8fcce3d53922fd223d6f5bd83d696d44175b69f18"
   },
-  {
-    "name": "u7981",
-    "unicode": "1F232",
+  "u7981": {
+    "category": "symbols",
+    "moji": "🈲",
+    "unicodeVersion": "6.0",
     "digest": "adbe12601b22972003ddebcb0bd1532b979aa9c78bfdc147511854b5014eabc0"
   },
-  {
-    "name": "u7a7a",
-    "unicode": "1F233",
+  "u7a7a": {
+    "category": "symbols",
+    "moji": "🈳",
+    "unicodeVersion": "6.0",
     "digest": "b9ee0ec7bb0b86c3eb73d4dbbb91848c427bf356ae30a263b9b44bd9bd784482"
   },
-  {
-    "name": "umbrella",
-    "unicode": "2614",
+  "umbrella": {
+    "category": "nature",
+    "moji": "☔",
+    "unicodeVersion": "4.0",
     "digest": "0328a2f48b7df47905e2655460e524c0794ef12d3d7c32a049a10892d5662f77"
   },
-  {
-    "name": "umbrella2",
-    "unicode": "2602",
+  "umbrella2": {
+    "category": "nature",
+    "moji": "☂",
+    "unicodeVersion": "1.1",
     "digest": "2f6a58110dc590480a822a3ffa2b5bc86f295e0c994a4a632837d25d4cf9fc58"
   },
-  {
-    "name": "unamused",
-    "unicode": "1F612",
+  "unamused": {
+    "category": "people",
+    "moji": "😒",
+    "unicodeVersion": "6.0",
     "digest": "0d597088e3e7880918d0166e5c69243b18fe64afa31685c39bfdbc71494aa132"
   },
-  {
-    "name": "underage",
-    "unicode": "1F51E",
+  "underage": {
+    "category": "symbols",
+    "moji": "🔞",
+    "unicodeVersion": "6.0",
     "digest": "b6b194614ca714ac2b1c2c17b75fe5922c7fdadb3d1157ba89ab2a5d03494a67"
   },
-  {
-    "name": "unicorn",
-    "unicode": "1F984",
+  "unicorn": {
+    "category": "nature",
+    "moji": "🦄",
+    "unicodeVersion": "8.0",
     "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
   },
-  {
-    "name": "unicorn_face",
-    "unicode": "1F984",
-    "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
-  },
-  {
-    "name": "unlock",
-    "unicode": "1F513",
+  "unlock": {
+    "category": "objects",
+    "moji": "🔓",
+    "unicodeVersion": "6.0",
     "digest": "9554ef3a6a315938b873e77970d9b0212e61f13c6cc36e4f17f87acc930a9a53"
   },
-  {
-    "name": "up",
-    "unicode": "1F199",
+  "up": {
+    "category": "symbols",
+    "moji": "🆙",
+    "unicodeVersion": "6.0",
     "digest": "ff2554ccf08c7208b38794c5fa3d9a93a46ff191a49401195d8f740846121906"
   },
-  {
-    "name": "upside_down",
-    "unicode": "1F643",
-    "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
-  },
-  {
-    "name": "upside_down_face",
-    "unicode": "1F643",
+  "upside_down": {
+    "category": "people",
+    "moji": "🙃",
+    "unicodeVersion": "8.0",
     "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
   },
-  {
-    "name": "urn",
-    "unicode": "26B1",
+  "urn": {
+    "category": "objects",
+    "moji": "⚱",
+    "unicodeVersion": "4.1",
     "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
   },
-  {
-    "name": "funeral_urn",
-    "unicode": "26B1",
-    "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
-  },
-  {
-    "name": "v",
-    "unicode": "270C",
+  "v": {
+    "category": "people",
+    "moji": "✌",
+    "unicodeVersion": "1.1",
     "digest": "9825bf440df289a8edf8ede494e8c778dc63c95f967f4d7bbea3245cf4f558ec"
   },
-  {
-    "name": "v_tone1",
-    "unicode": "270C-1F3FB",
+  "v_tone1": {
+    "category": "people",
+    "moji": "✌🏻",
+    "unicodeVersion": "8.0",
     "digest": "76e358250d9ca519b60b8d7b6a32900700d784433dcc609e9442254a410f6e37"
   },
-  {
-    "name": "v_tone2",
-    "unicode": "270C-1F3FC",
+  "v_tone2": {
+    "category": "people",
+    "moji": "✌🏼",
+    "unicodeVersion": "8.0",
     "digest": "4081b674be8416136022523fa9f29ec70a0f7e3aa05ca13152606609f3fd003c"
   },
-  {
-    "name": "v_tone3",
-    "unicode": "270C-1F3FD",
+  "v_tone3": {
+    "category": "people",
+    "moji": "✌🏽",
+    "unicodeVersion": "8.0",
     "digest": "b6afb3a4c78384280610b953592d378241c75597a82aa6d16c86a993f8d8f3b0"
   },
-  {
-    "name": "v_tone4",
-    "unicode": "270C-1F3FE",
+  "v_tone4": {
+    "category": "people",
+    "moji": "✌🏾",
+    "unicodeVersion": "8.0",
     "digest": "7ddc3cdd0138da2c8d7f6d8257ffdb8801496043e8a2395f93b0663447ac7fce"
   },
-  {
-    "name": "v_tone5",
-    "unicode": "270C-1F3FF",
+  "v_tone5": {
+    "category": "people",
+    "moji": "✌🏿",
+    "unicodeVersion": "8.0",
     "digest": "a85dc5c589f0d1cf32f8bfa5c82e5c11c40b35439636914686a2f06f7359f539"
   },
-  {
-    "name": "vertical_traffic_light",
-    "unicode": "1F6A6",
+  "vertical_traffic_light": {
+    "category": "travel",
+    "moji": "🚦",
+    "unicodeVersion": "6.0",
     "digest": "8cfd49a8f96b15a8313ef855f2e234ea3fa58332e68896dea34760740de9f020"
   },
-  {
-    "name": "vhs",
-    "unicode": "1F4FC",
+  "vhs": {
+    "category": "objects",
+    "moji": "📼",
+    "unicodeVersion": "6.0",
     "digest": "3fb1acaf25805cf86f8d40ee2c17cf25da587b7ca93b931167ab43fce041eee8"
   },
-  {
-    "name": "vibration_mode",
-    "unicode": "1F4F3",
+  "vibration_mode": {
+    "category": "symbols",
+    "moji": "📳",
+    "unicodeVersion": "6.0",
     "digest": "c9a8899222f46fe51dd8cee3e59f77c48268f0b7cfae2bcb34a791213acb1755"
   },
-  {
-    "name": "video_camera",
-    "unicode": "1F4F9",
+  "video_camera": {
+    "category": "objects",
+    "moji": "📹",
+    "unicodeVersion": "6.0",
     "digest": "62e56f26c286a7964ef1021f0f23fcb4b38cdcfb5b5af569b472340c412c619a"
   },
-  {
-    "name": "video_game",
-    "unicode": "1F3AE",
+  "video_game": {
+    "category": "activity",
+    "moji": "🎮",
+    "unicodeVersion": "6.0",
     "digest": "2787e302aa9e6fd7e9dc382c9bc7f5fbf244ef4940e08a4f9e80d33324f3032e"
   },
-  {
-    "name": "violin",
-    "unicode": "1F3BB",
+  "violin": {
+    "category": "activity",
+    "moji": "🎻",
+    "unicodeVersion": "6.0",
     "digest": "1e69d531ce2b5d5bf1dd9470187dbbe76f479d14428834b6a9e2bf5296dc0ec9"
   },
-  {
-    "name": "virgo",
-    "unicode": "264D",
+  "virgo": {
+    "category": "symbols",
+    "moji": "♍",
+    "unicodeVersion": "1.1",
     "digest": "0f75e9c228bc467fd0cec0f93f0e087c943bc5fb1d945fb0d4de53d07718388e"
   },
-  {
-    "name": "volcano",
-    "unicode": "1F30B",
+  "volcano": {
+    "category": "travel",
+    "moji": "🌋",
+    "unicodeVersion": "6.0",
     "digest": "41c92ef88ca533df342a0ebe59d2b676873bfa944c3988495b8a96060a9b8e16"
   },
-  {
-    "name": "volleyball",
-    "unicode": "1F3D0",
+  "volleyball": {
+    "category": "activity",
+    "moji": "🏐",
+    "unicodeVersion": "8.0",
     "digest": "774a83357f7aee890b4d4383236f0a90946dbd7c86aaabadc5753dcc9b4c9d69"
   },
-  {
-    "name": "vs",
-    "unicode": "1F19A",
+  "vs": {
+    "category": "symbols",
+    "moji": "🆚",
+    "unicodeVersion": "6.0",
     "digest": "ac943e4c737459c2e1adbac8b71d3fdaebb704dbaf5713012e7a77beb09db1ef"
   },
-  {
-    "name": "vulcan",
-    "unicode": "1F596",
-    "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
-  },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers",
-    "unicode": "1F596",
+  "vulcan": {
+    "category": "people",
+    "moji": "🖖",
+    "unicodeVersion": "7.0",
     "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
   },
-  {
-    "name": "vulcan_tone1",
-    "unicode": "1F596-1F3FB",
+  "vulcan_tone1": {
+    "category": "people",
+    "moji": "🖖🏻",
+    "unicodeVersion": "8.0",
     "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
   },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1",
-    "unicode": "1F596-1F3FB",
-    "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
-  },
-  {
-    "name": "vulcan_tone2",
-    "unicode": "1F596-1F3FC",
-    "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
-  },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2",
-    "unicode": "1F596-1F3FC",
+  "vulcan_tone2": {
+    "category": "people",
+    "moji": "🖖🏼",
+    "unicodeVersion": "8.0",
     "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
   },
-  {
-    "name": "vulcan_tone3",
-    "unicode": "1F596-1F3FD",
+  "vulcan_tone3": {
+    "category": "people",
+    "moji": "🖖🏽",
+    "unicodeVersion": "8.0",
     "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
   },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3",
-    "unicode": "1F596-1F3FD",
-    "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
-  },
-  {
-    "name": "vulcan_tone4",
-    "unicode": "1F596-1F3FE",
-    "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
-  },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4",
-    "unicode": "1F596-1F3FE",
+  "vulcan_tone4": {
+    "category": "people",
+    "moji": "🖖🏾",
+    "unicodeVersion": "8.0",
     "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
   },
-  {
-    "name": "vulcan_tone5",
-    "unicode": "1F596-1F3FF",
+  "vulcan_tone5": {
+    "category": "people",
+    "moji": "🖖🏿",
+    "unicodeVersion": "8.0",
     "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
   },
-  {
-    "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5",
-    "unicode": "1F596-1F3FF",
-    "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
-  },
-  {
-    "name": "walking",
-    "unicode": "1F6B6",
+  "walking": {
+    "category": "people",
+    "moji": "🚶",
+    "unicodeVersion": "6.0",
     "digest": "ae77471fe1e8a734d11711cdb589f64347c35d6ee2fc10f6db16ac550c0557fa"
   },
-  {
-    "name": "walking_tone1",
-    "unicode": "1F6B6-1F3FB",
+  "walking_tone1": {
+    "category": "people",
+    "moji": "🚶🏻",
+    "unicodeVersion": "8.0",
     "digest": "3de871c234e1340ccf95338df7babd94d175cfcb17a57b5a74d950e0a31f03b1"
   },
-  {
-    "name": "walking_tone2",
-    "unicode": "1F6B6-1F3FC",
+  "walking_tone2": {
+    "category": "people",
+    "moji": "🚶🏼",
+    "unicodeVersion": "8.0",
     "digest": "620eb7bfb753a331a5822b02bdaf08d8dde7b573efd210287a3d3dfdd84a40b9"
   },
-  {
-    "name": "walking_tone3",
-    "unicode": "1F6B6-1F3FD",
+  "walking_tone3": {
+    "category": "people",
+    "moji": "🚶🏽",
+    "unicodeVersion": "8.0",
     "digest": "ff39545acc2256006128f8c186433c28052b8c9aaec46fe06f25cff02c71f6b8"
   },
-  {
-    "name": "walking_tone4",
-    "unicode": "1F6B6-1F3FE",
+  "walking_tone4": {
+    "category": "people",
+    "moji": "🚶🏾",
+    "unicodeVersion": "8.0",
     "digest": "a9499d142392977a9b9e54fb957952359e9bdffce7ec2f1e8320523d185fb066"
   },
-  {
-    "name": "walking_tone5",
-    "unicode": "1F6B6-1F3FF",
+  "walking_tone5": {
+    "category": "people",
+    "moji": "🚶🏿",
+    "unicodeVersion": "8.0",
     "digest": "b47a4c48ce40298f842f454fc1abccae70f69725d73ee2c80e4018f4c4065d7d"
   },
-  {
-    "name": "waning_crescent_moon",
-    "unicode": "1F318",
+  "waning_crescent_moon": {
+    "category": "nature",
+    "moji": "🌘",
+    "unicodeVersion": "6.0",
     "digest": "2ec7896eefcf821e0ea013556a17af59e997503662c07f080d0a84ab13ef4cf1"
   },
-  {
-    "name": "waning_gibbous_moon",
-    "unicode": "1F316",
+  "waning_gibbous_moon": {
+    "category": "nature",
+    "moji": "🌖",
+    "unicodeVersion": "6.0",
     "digest": "ce2f5aca8fccdacaaf174d10da4e493e853e4608cc4d159aa3081d108a8b58d5"
   },
-  {
-    "name": "warning",
-    "unicode": "26A0",
+  "warning": {
+    "category": "symbols",
+    "moji": "⚠",
+    "unicodeVersion": "4.0",
     "digest": "745f1d203958f42bf37ecb5909cd0819934e300308ba0ff20964c8c203092f90"
   },
-  {
-    "name": "wastebasket",
-    "unicode": "1F5D1",
+  "wastebasket": {
+    "category": "objects",
+    "moji": "🗑",
+    "unicodeVersion": "7.0",
     "digest": "221a1b6d9975051038d9d97e18a16556cdf4254a6bca4c29bf1c51f306c79f2a"
   },
-  {
-    "name": "watch",
-    "unicode": "231A",
+  "watch": {
+    "category": "objects",
+    "moji": "⌚",
+    "unicodeVersion": "1.1",
     "digest": "acc0c96751404a789b3085f10425cf34f942185215df459515d2439cde3efc6b"
   },
-  {
-    "name": "water_buffalo",
-    "unicode": "1F403",
+  "water_buffalo": {
+    "category": "nature",
+    "moji": "🐃",
+    "unicodeVersion": "6.0",
     "digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1"
   },
-  {
-    "name": "water_polo",
-    "unicode": "1F93D",
+  "water_polo": {
+    "category": "activity",
+    "moji": "🤽",
+    "unicodeVersion": "9.0",
     "digest": "fc77e1d2a84a9f4cf0cf19c1ea10cf137cf0940b9103a523121eda87677ad148"
   },
-  {
-    "name": "water_polo_tone1",
-    "unicode": "1F93D-1F3FB",
+  "water_polo_tone1": {
+    "category": "activity",
+    "moji": "🤽🏻",
+    "unicodeVersion": "9.0",
     "digest": "3be28384edd29ada8109f07720d601a9d5866ed63e6234efe9ee1a194ed5d0c5"
   },
-  {
-    "name": "water_polo_tone2",
-    "unicode": "1F93D-1F3FC",
+  "water_polo_tone2": {
+    "category": "activity",
+    "moji": "🤽🏼",
+    "unicodeVersion": "9.0",
     "digest": "afcd3f28c6719f869ca79a6fd1ccade2ea976ade844fbc1081fc72865bcb652f"
   },
-  {
-    "name": "water_polo_tone3",
-    "unicode": "1F93D-1F3FD",
+  "water_polo_tone3": {
+    "category": "activity",
+    "moji": "🤽🏽",
+    "unicodeVersion": "9.0",
     "digest": "d19481c9b82d9413e99c2652e020fd763f2b54408dedaffec8dfe80973ded407"
   },
-  {
-    "name": "water_polo_tone4",
-    "unicode": "1F93D-1F3FE",
+  "water_polo_tone4": {
+    "category": "activity",
+    "moji": "🤽🏾",
+    "unicodeVersion": "9.0",
     "digest": "375972d882b627e8d525e632e58b30346fc3e01858d7d08d62a9d3bf8132bbc7"
   },
-  {
-    "name": "water_polo_tone5",
-    "unicode": "1F93D-1F3FF",
+  "water_polo_tone5": {
+    "category": "activity",
+    "moji": "🤽🏿",
+    "unicodeVersion": "9.0",
     "digest": "a8e1ced1c5382a8147a1d1801a133cada9a0e52e41de6272e56c3c1f426f6048"
   },
-  {
-    "name": "watermelon",
-    "unicode": "1F349",
+  "watermelon": {
+    "category": "food",
+    "moji": "🍉",
+    "unicodeVersion": "6.0",
     "digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a"
   },
-  {
-    "name": "wave",
-    "unicode": "1F44B",
+  "wave": {
+    "category": "people",
+    "moji": "👋",
+    "unicodeVersion": "6.0",
     "digest": "cddbd764d471604446cbaca91f77f6c4119d1cfc2c856732ca0eaac4593cb736"
   },
-  {
-    "name": "wave_tone1",
-    "unicode": "1F44B-1F3FB",
+  "wave_tone1": {
+    "category": "people",
+    "moji": "👋🏻",
+    "unicodeVersion": "8.0",
     "digest": "cf40797437ddf68ec0275f337e6aac4bed81e28da7636d56c9f817ddf8e2b30a"
   },
-  {
-    "name": "wave_tone2",
-    "unicode": "1F44B-1F3FC",
+  "wave_tone2": {
+    "category": "people",
+    "moji": "👋🏼",
+    "unicodeVersion": "8.0",
     "digest": "12c8a3e82c03ee35a734c642be482ba2d9d5948dacf91ec1fda243316dd4a0d0"
   },
-  {
-    "name": "wave_tone3",
-    "unicode": "1F44B-1F3FD",
+  "wave_tone3": {
+    "category": "people",
+    "moji": "👋🏽",
+    "unicodeVersion": "8.0",
     "digest": "ebcaef43e21b475f76de811d4f4d1a67d9393973b57b03876e02164345a2ba4a"
   },
-  {
-    "name": "wave_tone4",
-    "unicode": "1F44B-1F3FE",
+  "wave_tone4": {
+    "category": "people",
+    "moji": "👋🏾",
+    "unicodeVersion": "8.0",
     "digest": "7df7b70cf76766836ba146c3d91b6104930c384450cf2688426e60c1c06a1fc8"
   },
-  {
-    "name": "wave_tone5",
-    "unicode": "1F44B-1F3FF",
+  "wave_tone5": {
+    "category": "people",
+    "moji": "👋🏿",
+    "unicodeVersion": "8.0",
     "digest": "8dfdba6aeff5d7dfd807467d431a137547726b34d021f1a5a0b74e155d270ea7"
   },
-  {
-    "name": "wavy_dash",
-    "unicode": "3030",
+  "wavy_dash": {
+    "category": "symbols",
+    "moji": "〰",
+    "unicodeVersion": "1.1",
     "digest": "7b1968474f01d12fd09a1f2572282927138d9e9d6a3642de4bf68af80a8c3738"
   },
-  {
-    "name": "waxing_crescent_moon",
-    "unicode": "1F312",
+  "waxing_crescent_moon": {
+    "category": "nature",
+    "moji": "🌒",
+    "unicodeVersion": "6.0",
     "digest": "852d7e55a19074d061fa3aa80d6b1e7e87a9280bdf44d94bbdbbe6d59178b1be"
   },
-  {
-    "name": "waxing_gibbous_moon",
-    "unicode": "1F314",
+  "waxing_gibbous_moon": {
+    "category": "nature",
+    "moji": "🌔",
+    "unicodeVersion": "6.0",
     "digest": "a3a1c7cc72521a3f74929789a90e1c35d81ac86e21225c9f844d718d8940e3b3"
   },
-  {
-    "name": "wc",
-    "unicode": "1F6BE",
+  "wc": {
+    "category": "symbols",
+    "moji": "🚾",
+    "unicodeVersion": "6.0",
     "digest": "4b95d54e0b53e4b705277917653503b32d6a143c2eaf6c547bc8e01c2dc23659"
   },
-  {
-    "name": "weary",
-    "unicode": "1F629",
+  "weary": {
+    "category": "people",
+    "moji": "😩",
+    "unicodeVersion": "6.0",
     "digest": "3528f85540996cd5b562efe5421c495fc1bb414dc797bc20062783ae1b730847"
   },
-  {
-    "name": "wedding",
-    "unicode": "1F492",
+  "wedding": {
+    "category": "travel",
+    "moji": "💒",
+    "unicodeVersion": "6.0",
     "digest": "980f3522cc4c19c3096e668032ea2cd19e7900cdc4b73bbb1c9b4c4d28dc78af"
   },
-  {
-    "name": "whale",
-    "unicode": "1F433",
+  "whale": {
+    "category": "nature",
+    "moji": "🐳",
+    "unicodeVersion": "6.0",
     "digest": "6368fe4bc4a7f68aa2bd5386686a5f1b159feacbec16d59515f2b6e5d01adfbd"
   },
-  {
-    "name": "whale2",
-    "unicode": "1F40B",
+  "whale2": {
+    "category": "nature",
+    "moji": "🐋",
+    "unicodeVersion": "6.0",
     "digest": "ccd3edf88167965f2abc18631ffb80e2532f728da35bc0c11144376685da18e8"
   },
-  {
-    "name": "wheel_of_dharma",
-    "unicode": "2638",
+  "wheel_of_dharma": {
+    "category": "symbols",
+    "moji": "☸",
+    "unicodeVersion": "1.1",
     "digest": "4a0a13fcd507b9621686c8090bf340aa8770c064e0e3eb576fbae1229000d6da"
   },
-  {
-    "name": "wheelchair",
-    "unicode": "267F",
+  "wheelchair": {
+    "category": "symbols",
+    "moji": "♿",
+    "unicodeVersion": "4.1",
     "digest": "f5250f2b4b5b4ffe6a6f77d30865c3f5d7173fc91aee547869589b2a96da91c8"
   },
-  {
-    "name": "white_check_mark",
-    "unicode": "2705",
+  "white_check_mark": {
+    "category": "symbols",
+    "moji": "✅",
+    "unicodeVersion": "6.0",
     "digest": "45eb17bde6e503f22c8579d6e4d507ad6557a15f9eaad14aa716ec9ba1540876"
   },
-  {
-    "name": "white_circle",
-    "unicode": "26AA",
+  "white_circle": {
+    "category": "symbols",
+    "moji": "⚪",
+    "unicodeVersion": "4.1",
     "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c"
   },
-  {
-    "name": "white_flower",
-    "unicode": "1F4AE",
+  "white_flower": {
+    "category": "symbols",
+    "moji": "💮",
+    "unicodeVersion": "6.0",
     "digest": "ace093b310eeefdecf4a4bdaf4fbcbb568457b0191ac80778a466ac5f3f4025a"
   },
-  {
-    "name": "white_large_square",
-    "unicode": "2B1C",
+  "white_large_square": {
+    "category": "symbols",
+    "moji": "⬜",
+    "unicodeVersion": "5.1",
     "digest": "0db6957ee9ff7325b534b730fc05345a63d4ed9060f0f816807d0dcf004baa3e"
   },
-  {
-    "name": "white_medium_small_square",
-    "unicode": "25FD",
+  "white_medium_small_square": {
+    "category": "symbols",
+    "moji": "◽",
+    "unicodeVersion": "3.2",
     "digest": "d79689981a7b38211c60a025a81e44fd39ac6ea4062e227cae3aab8f51572cd4"
   },
-  {
-    "name": "white_medium_square",
-    "unicode": "25FB",
+  "white_medium_square": {
+    "category": "symbols",
+    "moji": "◻",
+    "unicodeVersion": "3.2",
     "digest": "6c4ce26d3f69667219f29ea18b04f3e79373024426275f25936e09a683e9a4fc"
   },
-  {
-    "name": "white_small_square",
-    "unicode": "25AB",
+  "white_small_square": {
+    "category": "symbols",
+    "moji": "▫",
+    "unicodeVersion": "1.1",
     "digest": "ae0d35a6bbba4592b89b2f0f1f2d183efb2f93cf2a2136c0c195aab72f0bb1c8"
   },
-  {
-    "name": "white_square_button",
-    "unicode": "1F533",
+  "white_square_button": {
+    "category": "symbols",
+    "moji": "🔳",
+    "unicodeVersion": "6.0",
     "digest": "797f3d9e44e88e940ffc118e52d0f709eec2ef14b13bdf873ad4b0c96cc0b042"
   },
-  {
-    "name": "white_sun_cloud",
-    "unicode": "1F325",
-    "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
-  },
-  {
-    "name": "white_sun_behind_cloud",
-    "unicode": "1F325",
+  "white_sun_cloud": {
+    "category": "nature",
+    "moji": "🌥",
+    "unicodeVersion": "7.0",
     "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
   },
-  {
-    "name": "white_sun_rain_cloud",
-    "unicode": "1F326",
+  "white_sun_rain_cloud": {
+    "category": "nature",
+    "moji": "🌦",
+    "unicodeVersion": "7.0",
     "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
   },
-  {
-    "name": "white_sun_behind_cloud_with_rain",
-    "unicode": "1F326",
-    "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
-  },
-  {
-    "name": "white_sun_small_cloud",
-    "unicode": "1F324",
-    "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
-  },
-  {
-    "name": "white_sun_with_small_cloud",
-    "unicode": "1F324",
+  "white_sun_small_cloud": {
+    "category": "nature",
+    "moji": "🌤",
+    "unicodeVersion": "7.0",
     "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
   },
-  {
-    "name": "wilted_rose",
-    "unicode": "1F940",
+  "wilted_rose": {
+    "category": "nature",
+    "moji": "🥀",
+    "unicodeVersion": "9.0",
     "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
   },
-  {
-    "name": "wilted_flower",
-    "unicode": "1F940",
-    "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
-  },
-  {
-    "name": "wind_blowing_face",
-    "unicode": "1F32C",
+  "wind_blowing_face": {
+    "category": "nature",
+    "moji": "🌬",
+    "unicodeVersion": "7.0",
     "digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f"
   },
-  {
-    "name": "wind_chime",
-    "unicode": "1F390",
+  "wind_chime": {
+    "category": "objects",
+    "moji": "🎐",
+    "unicodeVersion": "6.0",
     "digest": "1b1b212fbd74a9edc62aee7ffab9bcf91d3a9f69bffb2be4b7fd527914c14ced"
   },
-  {
-    "name": "wine_glass",
-    "unicode": "1F377",
+  "wine_glass": {
+    "category": "food",
+    "moji": "🍷",
+    "unicodeVersion": "6.0",
     "digest": "d99107d6809386bc5e219aa58ee4930d27b7c3a6d2b10deb9f523df369f766d1"
   },
-  {
-    "name": "wink",
-    "unicode": "1F609",
+  "wink": {
+    "category": "people",
+    "moji": "😉",
+    "unicodeVersion": "6.0",
     "digest": "56e29994a47335a901d0c98fa141d26faae8f647a860517bd3615fa980921885"
   },
-  {
-    "name": "wolf",
-    "unicode": "1F43A",
+  "wolf": {
+    "category": "nature",
+    "moji": "🐺",
+    "unicodeVersion": "6.0",
     "digest": "4a983f5ec8ec0872fcde7890e17605b1229064e5e194b6fca1c4259068d1caed"
   },
-  {
-    "name": "woman",
-    "unicode": "1F469",
+  "woman": {
+    "category": "people",
+    "moji": "👩",
+    "unicodeVersion": "6.0",
     "digest": "a06a22a48eeb3aeb885321358fe234e97797ed33be17f52d232ce2830cfbcd97"
   },
-  {
-    "name": "woman_tone1",
-    "unicode": "1F469-1F3FB",
+  "woman_tone1": {
+    "category": "people",
+    "moji": "👩🏻",
+    "unicodeVersion": "8.0",
     "digest": "c2e4b135c1dac6a0b002569a6ccd9d098f6cb18481c68b5d9115e11241a0978d"
   },
-  {
-    "name": "woman_tone2",
-    "unicode": "1F469-1F3FC",
+  "woman_tone2": {
+    "category": "people",
+    "moji": "👩🏼",
+    "unicodeVersion": "8.0",
     "digest": "4848e650051214a53c4cd9f6d3d94158f77f65ecb34f891789de34ee0a713006"
   },
-  {
-    "name": "woman_tone3",
-    "unicode": "1F469-1F3FD",
+  "woman_tone3": {
+    "category": "people",
+    "moji": "👩🏽",
+    "unicodeVersion": "8.0",
     "digest": "b6f751ad47da019cdfb9d6d78f9610adb92120abf204c30df79a9150b57dbdee"
   },
-  {
-    "name": "woman_tone4",
-    "unicode": "1F469-1F3FE",
+  "woman_tone4": {
+    "category": "people",
+    "moji": "👩🏾",
+    "unicodeVersion": "8.0",
     "digest": "fd27d3a669dc34313fbfe518df7dc2ded3ade5dde695f8d773afe87bf8a8b0d4"
   },
-  {
-    "name": "woman_tone5",
-    "unicode": "1F469-1F3FF",
+  "woman_tone5": {
+    "category": "people",
+    "moji": "👩🏿",
+    "unicodeVersion": "8.0",
     "digest": "9ae9b14dfff40fa60a565d89479727feeba4fd6ffea9acb353a81b14aba751d4"
   },
-  {
-    "name": "womans_clothes",
-    "unicode": "1F45A",
+  "womans_clothes": {
+    "category": "people",
+    "moji": "👚",
+    "unicodeVersion": "6.0",
     "digest": "d12a27810780fe5cd8118ed4587e0c4e70dbe9bcd014c6866fe6a8c9c7c55698"
   },
-  {
-    "name": "womans_hat",
-    "unicode": "1F452",
+  "womans_hat": {
+    "category": "people",
+    "moji": "👒",
+    "unicodeVersion": "6.0",
     "digest": "52a0255b3483085bd125d39b74516ab6a81003964f44995c2fac821e7ff93086"
   },
-  {
-    "name": "womens",
-    "unicode": "1F6BA",
+  "womens": {
+    "category": "symbols",
+    "moji": "🚺",
+    "unicodeVersion": "6.0",
     "digest": "7e38964006f8b28dfa2b3e9b2b16553bb50c18a63455f556b0bff35ee172137e"
   },
-  {
-    "name": "worried",
-    "unicode": "1F61F",
+  "worried": {
+    "category": "people",
+    "moji": "😟",
+    "unicodeVersion": "6.1",
     "digest": "5a073985e1344bc34201ef94a491f7f2b946f5828c9fdbc57eeb2dcd87ac3a6b"
   },
-  {
-    "name": "wrench",
-    "unicode": "1F527",
+  "wrench": {
+    "category": "objects",
+    "moji": "🔧",
+    "unicodeVersion": "6.0",
     "digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4"
   },
-  {
-    "name": "wrestlers",
-    "unicode": "1F93C",
-    "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
-  },
-  {
-    "name": "wrestling",
-    "unicode": "1F93C",
+  "wrestlers": {
+    "category": "activity",
+    "moji": "🤼",
+    "unicodeVersion": "9.0",
     "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
   },
-  {
-    "name": "wrestlers_tone1",
-    "unicode": "1F93C-1F3FB",
+  "wrestlers_tone1": {
+    "category": "activity",
+    "moji": "🤼🏻",
+    "unicodeVersion": "9.0",
     "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
   },
-  {
-    "name": "wrestling_tone1",
-    "unicode": "1F93C-1F3FB",
-    "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
-  },
-  {
-    "name": "wrestlers_tone2",
-    "unicode": "1F93C-1F3FC",
-    "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
-  },
-  {
-    "name": "wrestling_tone2",
-    "unicode": "1F93C-1F3FC",
+  "wrestlers_tone2": {
+    "category": "activity",
+    "moji": "🤼🏼",
+    "unicodeVersion": "9.0",
     "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
   },
-  {
-    "name": "wrestlers_tone3",
-    "unicode": "1F93C-1F3FD",
+  "wrestlers_tone3": {
+    "category": "activity",
+    "moji": "🤼🏽",
+    "unicodeVersion": "9.0",
     "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
   },
-  {
-    "name": "wrestling_tone3",
-    "unicode": "1F93C-1F3FD",
-    "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
-  },
-  {
-    "name": "wrestlers_tone4",
-    "unicode": "1F93C-1F3FE",
-    "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
-  },
-  {
-    "name": "wrestling_tone4",
-    "unicode": "1F93C-1F3FE",
+  "wrestlers_tone4": {
+    "category": "activity",
+    "moji": "🤼🏾",
+    "unicodeVersion": "9.0",
     "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
   },
-  {
-    "name": "wrestlers_tone5",
-    "unicode": "1F93C-1F3FF",
+  "wrestlers_tone5": {
+    "category": "activity",
+    "moji": "🤼🏿",
+    "unicodeVersion": "9.0",
     "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
   },
-  {
-    "name": "wrestling_tone5",
-    "unicode": "1F93C-1F3FF",
-    "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
-  },
-  {
-    "name": "writing_hand",
-    "unicode": "270D",
+  "writing_hand": {
+    "category": "people",
+    "moji": "✍",
+    "unicodeVersion": "1.1",
     "digest": "110517ae4da5587e8b0662881658e27da4120bfacec54734fd6657831d4d782f"
   },
-  {
-    "name": "writing_hand_tone1",
-    "unicode": "270D-1F3FB",
+  "writing_hand_tone1": {
+    "category": "people",
+    "moji": "✍🏻",
+    "unicodeVersion": "8.0",
     "digest": "2c7e2108e1990490b681343c1b01b4183d4f18fbdef792f113b2f87595e0dad0"
   },
-  {
-    "name": "writing_hand_tone2",
-    "unicode": "270D-1F3FC",
+  "writing_hand_tone2": {
+    "category": "people",
+    "moji": "✍🏼",
+    "unicodeVersion": "8.0",
     "digest": "87ec8d44f472d301adbcbd50d8c852b609e46584057f59cc1527401db363c1bf"
   },
-  {
-    "name": "writing_hand_tone3",
-    "unicode": "270D-1F3FD",
+  "writing_hand_tone3": {
+    "category": "people",
+    "moji": "✍🏽",
+    "unicodeVersion": "8.0",
     "digest": "4a48ddef91f7264e8fa9cca223554db22b3a2e3153e94b88d146644ea6dd661e"
   },
-  {
-    "name": "writing_hand_tone4",
-    "unicode": "270D-1F3FE",
+  "writing_hand_tone4": {
+    "category": "people",
+    "moji": "✍🏾",
+    "unicodeVersion": "8.0",
     "digest": "e5254564a1f91e42ee59f359d8cd26f52abdc04dca8f3b37cb2f140cb7f71390"
   },
-  {
-    "name": "writing_hand_tone5",
-    "unicode": "270D-1F3FF",
+  "writing_hand_tone5": {
+    "category": "people",
+    "moji": "✍🏿",
+    "unicodeVersion": "8.0",
     "digest": "61299bf86d83d323ca3e6052c535ae66c6f7b3d9866a37db0464223b8bc28523"
   },
-  {
-    "name": "x",
-    "unicode": "274C",
+  "x": {
+    "category": "symbols",
+    "moji": "❌",
+    "unicodeVersion": "6.0",
     "digest": "3e5a7918e31ddefdf1ce73972365e2f0bfd2917d6a450c1a278c108349c9425d"
   },
-  {
-    "name": "yellow_heart",
-    "unicode": "1F49B",
+  "yellow_heart": {
+    "category": "symbols",
+    "moji": "💛",
+    "unicodeVersion": "6.0",
     "digest": "a1098f2f04c29754cc9974324508386787d4d803b57cf691d42de414cb2679d6"
   },
-  {
-    "name": "yen",
-    "unicode": "1F4B4",
+  "yen": {
+    "category": "objects",
+    "moji": "💴",
+    "unicodeVersion": "6.0",
     "digest": "944daaeb3f6369c807c0e63b106cee1360040f7800a70c0d942a992f25a55da7"
   },
-  {
-    "name": "yin_yang",
-    "unicode": "262F",
+  "yin_yang": {
+    "category": "symbols",
+    "moji": "☯",
+    "unicodeVersion": "1.1",
     "digest": "5ee8d13dacf41306a09237bfcff6abeef110331b40eb7d6e80600628c1327545"
   },
-  {
-    "name": "yum",
-    "unicode": "1F60B",
+  "yum": {
+    "category": "people",
+    "moji": "😋",
+    "unicodeVersion": "6.0",
     "digest": "31a89088c21bd7a74a3a26d731a907d1bc49436300a9f9c55248703cf7ef44c7"
   },
-  {
-    "name": "zap",
-    "unicode": "26A1",
+  "zap": {
+    "category": "nature",
+    "moji": "⚡",
+    "unicodeVersion": "4.0",
     "digest": "9f8144ae6f866129aea41bbf694b0c858ef9352a139969e57cd8db73385f52c3"
   },
-  {
-    "name": "zero",
-    "unicode": "0030-20E3",
+  "zero": {
+    "category": "symbols",
+    "moji": "0️⃣",
+    "unicodeVersion": "3.0",
     "digest": "1b27b5c904defadbdd28ace67a6be5c277ff043297db7cd9f672bbf84e37fa1a"
   },
-  {
-    "name": "zipper_mouth",
-    "unicode": "1F910",
-    "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
-  },
-  {
-    "name": "zipper_mouth_face",
-    "unicode": "1F910",
+  "zipper_mouth": {
+    "category": "people",
+    "moji": "🤐",
+    "unicodeVersion": "8.0",
     "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
   },
-  {
-    "name": "zzz",
-    "unicode": "1F4A4",
+  "zzz": {
+    "category": "people",
+    "moji": "💤",
+    "unicodeVersion": "6.0",
     "digest": "b3313d0c44a59fa9d4ce9f7eb4d07ff71dfc8bb01798154250f27cdcf3c693b5"
   }
-]
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/fixtures/emojis/emoji-unicode-version-map.json b/fixtures/emojis/emoji-unicode-version-map.json
new file mode 100644
index 0000000000000000000000000000000000000000..5164fe3942672cb45d077b799028804f0fcdfd1d
--- /dev/null
+++ b/fixtures/emojis/emoji-unicode-version-map.json
@@ -0,0 +1,2377 @@
+{
+	"100": "6.0",
+	"1234": "6.0",
+	"grinning": "6.1",
+	"grin": "6.0",
+	"joy": "6.0",
+	"rofl": "9.0",
+	"rolling_on_the_floor_laughing": "9.0",
+	"smiley": "6.0",
+	"smile": "6.0",
+	"sweat_smile": "6.0",
+	"laughing": "6.0",
+	"satisfied": "6.0",
+	"wink": "6.0",
+	"blush": "6.0",
+	"yum": "6.0",
+	"sunglasses": "6.0",
+	"heart_eyes": "6.0",
+	"kissing_heart": "6.0",
+	"kissing": "6.1",
+	"kissing_smiling_eyes": "6.1",
+	"kissing_closed_eyes": "6.0",
+	"relaxed": "1.1",
+	"slight_smile": "7.0",
+	"slightly_smiling_face": "7.0",
+	"hugging": "8.0",
+	"hugging_face": "8.0",
+	"thinking": "8.0",
+	"thinking_face": "8.0",
+	"neutral_face": "6.0",
+	"expressionless": "6.1",
+	"no_mouth": "6.0",
+	"rolling_eyes": "8.0",
+	"face_with_rolling_eyes": "8.0",
+	"smirk": "6.0",
+	"persevere": "6.0",
+	"disappointed_relieved": "6.0",
+	"open_mouth": "6.1",
+	"zipper_mouth": "8.0",
+	"zipper_mouth_face": "8.0",
+	"hushed": "6.1",
+	"sleepy": "6.0",
+	"tired_face": "6.0",
+	"sleeping": "6.1",
+	"relieved": "6.0",
+	"nerd": "8.0",
+	"nerd_face": "8.0",
+	"stuck_out_tongue": "6.1",
+	"stuck_out_tongue_winking_eye": "6.0",
+	"stuck_out_tongue_closed_eyes": "6.0",
+	"drooling_face": "9.0",
+	"drool": "9.0",
+	"unamused": "6.0",
+	"sweat": "6.0",
+	"pensive": "6.0",
+	"confused": "6.1",
+	"upside_down": "8.0",
+	"upside_down_face": "8.0",
+	"money_mouth": "8.0",
+	"money_mouth_face": "8.0",
+	"astonished": "6.0",
+	"frowning2": "1.1",
+	"white_frowning_face": "1.1",
+	"slight_frown": "7.0",
+	"slightly_frowning_face": "7.0",
+	"confounded": "6.0",
+	"disappointed": "6.0",
+	"worried": "6.1",
+	"triumph": "6.0",
+	"cry": "6.0",
+	"sob": "6.0",
+	"frowning": "6.1",
+	"anguished": "6.1",
+	"fearful": "6.0",
+	"weary": "6.0",
+	"grimacing": "6.1",
+	"cold_sweat": "6.0",
+	"scream": "6.0",
+	"flushed": "6.0",
+	"dizzy_face": "6.0",
+	"rage": "6.0",
+	"angry": "6.0",
+	"innocent": "6.0",
+	"cowboy": "9.0",
+	"face_with_cowboy_hat": "9.0",
+	"clown": "9.0",
+	"clown_face": "9.0",
+	"lying_face": "9.0",
+	"liar": "9.0",
+	"mask": "6.0",
+	"thermometer_face": "8.0",
+	"face_with_thermometer": "8.0",
+	"head_bandage": "8.0",
+	"face_with_head_bandage": "8.0",
+	"nauseated_face": "9.0",
+	"sick": "9.0",
+	"sneezing_face": "9.0",
+	"sneeze": "9.0",
+	"smiling_imp": "6.0",
+	"imp": "6.0",
+	"japanese_ogre": "6.0",
+	"japanese_goblin": "6.0",
+	"skull": "6.0",
+	"skeleton": "6.0",
+	"skull_crossbones": "1.1",
+	"skull_and_crossbones": "1.1",
+	"ghost": "6.0",
+	"alien": "6.0",
+	"space_invader": "6.0",
+	"robot": "8.0",
+	"robot_face": "8.0",
+	"poop": "6.0",
+	"shit": "6.0",
+	"hankey": "6.0",
+	"poo": "6.0",
+	"smiley_cat": "6.0",
+	"smile_cat": "6.0",
+	"joy_cat": "6.0",
+	"heart_eyes_cat": "6.0",
+	"smirk_cat": "6.0",
+	"kissing_cat": "6.0",
+	"scream_cat": "6.0",
+	"crying_cat_face": "6.0",
+	"pouting_cat": "6.0",
+	"see_no_evil": "6.0",
+	"hear_no_evil": "6.0",
+	"speak_no_evil": "6.0",
+	"boy": "6.0",
+	"boy_tone1": "8.0",
+	"boy_tone2": "8.0",
+	"boy_tone3": "8.0",
+	"boy_tone4": "8.0",
+	"boy_tone5": "8.0",
+	"girl": "6.0",
+	"girl_tone1": "8.0",
+	"girl_tone2": "8.0",
+	"girl_tone3": "8.0",
+	"girl_tone4": "8.0",
+	"girl_tone5": "8.0",
+	"man": "6.0",
+	"man_tone1": "8.0",
+	"man_tone2": "8.0",
+	"man_tone3": "8.0",
+	"man_tone4": "8.0",
+	"man_tone5": "8.0",
+	"woman": "6.0",
+	"woman_tone1": "8.0",
+	"woman_tone2": "8.0",
+	"woman_tone3": "8.0",
+	"woman_tone4": "8.0",
+	"woman_tone5": "8.0",
+	"older_man": "6.0",
+	"older_man_tone1": "8.0",
+	"older_man_tone2": "8.0",
+	"older_man_tone3": "8.0",
+	"older_man_tone4": "8.0",
+	"older_man_tone5": "8.0",
+	"older_woman": "6.0",
+	"grandma": "6.0",
+	"older_woman_tone1": "8.0",
+	"grandma_tone1": "8.0",
+	"older_woman_tone2": "8.0",
+	"grandma_tone2": "8.0",
+	"older_woman_tone3": "8.0",
+	"grandma_tone3": "8.0",
+	"older_woman_tone4": "8.0",
+	"grandma_tone4": "8.0",
+	"older_woman_tone5": "8.0",
+	"grandma_tone5": "8.0",
+	"baby": "6.0",
+	"baby_tone1": "8.0",
+	"baby_tone2": "8.0",
+	"baby_tone3": "8.0",
+	"baby_tone4": "8.0",
+	"baby_tone5": "8.0",
+	"angel": "6.0",
+	"angel_tone1": "8.0",
+	"angel_tone2": "8.0",
+	"angel_tone3": "8.0",
+	"angel_tone4": "8.0",
+	"angel_tone5": "8.0",
+	"cop": "6.0",
+	"cop_tone1": "8.0",
+	"cop_tone2": "8.0",
+	"cop_tone3": "8.0",
+	"cop_tone4": "8.0",
+	"cop_tone5": "8.0",
+	"spy": "7.0",
+	"sleuth_or_spy": "7.0",
+	"spy_tone1": "8.0",
+	"sleuth_or_spy_tone1": "8.0",
+	"spy_tone2": "8.0",
+	"sleuth_or_spy_tone2": "8.0",
+	"spy_tone3": "8.0",
+	"sleuth_or_spy_tone3": "8.0",
+	"spy_tone4": "8.0",
+	"sleuth_or_spy_tone4": "8.0",
+	"spy_tone5": "8.0",
+	"sleuth_or_spy_tone5": "8.0",
+	"guardsman": "6.0",
+	"guardsman_tone1": "8.0",
+	"guardsman_tone2": "8.0",
+	"guardsman_tone3": "8.0",
+	"guardsman_tone4": "8.0",
+	"guardsman_tone5": "8.0",
+	"construction_worker": "6.0",
+	"construction_worker_tone1": "8.0",
+	"construction_worker_tone2": "8.0",
+	"construction_worker_tone3": "8.0",
+	"construction_worker_tone4": "8.0",
+	"construction_worker_tone5": "8.0",
+	"man_with_turban": "6.0",
+	"man_with_turban_tone1": "8.0",
+	"man_with_turban_tone2": "8.0",
+	"man_with_turban_tone3": "8.0",
+	"man_with_turban_tone4": "8.0",
+	"man_with_turban_tone5": "8.0",
+	"person_with_blond_hair": "6.0",
+	"person_with_blond_hair_tone1": "8.0",
+	"person_with_blond_hair_tone2": "8.0",
+	"person_with_blond_hair_tone3": "8.0",
+	"person_with_blond_hair_tone4": "8.0",
+	"person_with_blond_hair_tone5": "8.0",
+	"santa": "6.0",
+	"santa_tone1": "8.0",
+	"santa_tone2": "8.0",
+	"santa_tone3": "8.0",
+	"santa_tone4": "8.0",
+	"santa_tone5": "8.0",
+	"mrs_claus": "9.0",
+	"mother_christmas": "9.0",
+	"mrs_claus_tone1": "9.0",
+	"mother_christmas_tone1": "9.0",
+	"mrs_claus_tone2": "9.0",
+	"mother_christmas_tone2": "9.0",
+	"mrs_claus_tone3": "9.0",
+	"mother_christmas_tone3": "9.0",
+	"mrs_claus_tone4": "9.0",
+	"mother_christmas_tone4": "9.0",
+	"mrs_claus_tone5": "9.0",
+	"mother_christmas_tone5": "9.0",
+	"princess": "6.0",
+	"princess_tone1": "8.0",
+	"princess_tone2": "8.0",
+	"princess_tone3": "8.0",
+	"princess_tone4": "8.0",
+	"princess_tone5": "8.0",
+	"prince": "9.0",
+	"prince_tone1": "9.0",
+	"prince_tone2": "9.0",
+	"prince_tone3": "9.0",
+	"prince_tone4": "9.0",
+	"prince_tone5": "9.0",
+	"bride_with_veil": "6.0",
+	"bride_with_veil_tone1": "8.0",
+	"bride_with_veil_tone2": "8.0",
+	"bride_with_veil_tone3": "8.0",
+	"bride_with_veil_tone4": "8.0",
+	"bride_with_veil_tone5": "8.0",
+	"man_in_tuxedo": "9.0",
+	"man_in_tuxedo_tone1": "9.0",
+	"tuxedo_tone1": "9.0",
+	"man_in_tuxedo_tone2": "9.0",
+	"tuxedo_tone2": "9.0",
+	"man_in_tuxedo_tone3": "9.0",
+	"tuxedo_tone3": "9.0",
+	"man_in_tuxedo_tone4": "9.0",
+	"tuxedo_tone4": "9.0",
+	"man_in_tuxedo_tone5": "9.0",
+	"tuxedo_tone5": "9.0",
+	"pregnant_woman": "9.0",
+	"expecting_woman": "9.0",
+	"pregnant_woman_tone1": "9.0",
+	"expecting_woman_tone1": "9.0",
+	"pregnant_woman_tone2": "9.0",
+	"expecting_woman_tone2": "9.0",
+	"pregnant_woman_tone3": "9.0",
+	"expecting_woman_tone3": "9.0",
+	"pregnant_woman_tone4": "9.0",
+	"expecting_woman_tone4": "9.0",
+	"pregnant_woman_tone5": "9.0",
+	"expecting_woman_tone5": "9.0",
+	"man_with_gua_pi_mao": "6.0",
+	"man_with_gua_pi_mao_tone1": "8.0",
+	"man_with_gua_pi_mao_tone2": "8.0",
+	"man_with_gua_pi_mao_tone3": "8.0",
+	"man_with_gua_pi_mao_tone4": "8.0",
+	"man_with_gua_pi_mao_tone5": "8.0",
+	"person_frowning": "6.0",
+	"person_frowning_tone1": "8.0",
+	"person_frowning_tone2": "8.0",
+	"person_frowning_tone3": "8.0",
+	"person_frowning_tone4": "8.0",
+	"person_frowning_tone5": "8.0",
+	"person_with_pouting_face": "6.0",
+	"person_with_pouting_face_tone1": "8.0",
+	"person_with_pouting_face_tone2": "8.0",
+	"person_with_pouting_face_tone3": "8.0",
+	"person_with_pouting_face_tone4": "8.0",
+	"person_with_pouting_face_tone5": "8.0",
+	"no_good": "6.0",
+	"no_good_tone1": "8.0",
+	"no_good_tone2": "8.0",
+	"no_good_tone3": "8.0",
+	"no_good_tone4": "8.0",
+	"no_good_tone5": "8.0",
+	"ok_woman": "6.0",
+	"ok_woman_tone1": "8.0",
+	"ok_woman_tone2": "8.0",
+	"ok_woman_tone3": "8.0",
+	"ok_woman_tone4": "8.0",
+	"ok_woman_tone5": "8.0",
+	"information_desk_person": "6.0",
+	"information_desk_person_tone1": "8.0",
+	"information_desk_person_tone2": "8.0",
+	"information_desk_person_tone3": "8.0",
+	"information_desk_person_tone4": "8.0",
+	"information_desk_person_tone5": "8.0",
+	"raising_hand": "6.0",
+	"raising_hand_tone1": "8.0",
+	"raising_hand_tone2": "8.0",
+	"raising_hand_tone3": "8.0",
+	"raising_hand_tone4": "8.0",
+	"raising_hand_tone5": "8.0",
+	"bow": "6.0",
+	"bow_tone1": "8.0",
+	"bow_tone2": "8.0",
+	"bow_tone3": "8.0",
+	"bow_tone4": "8.0",
+	"bow_tone5": "8.0",
+	"face_palm": "9.0",
+	"facepalm": "9.0",
+	"face_palm_tone1": "9.0",
+	"facepalm_tone1": "9.0",
+	"face_palm_tone2": "9.0",
+	"facepalm_tone2": "9.0",
+	"face_palm_tone3": "9.0",
+	"facepalm_tone3": "9.0",
+	"face_palm_tone4": "9.0",
+	"facepalm_tone4": "9.0",
+	"face_palm_tone5": "9.0",
+	"facepalm_tone5": "9.0",
+	"shrug": "9.0",
+	"shrug_tone1": "9.0",
+	"shrug_tone2": "9.0",
+	"shrug_tone3": "9.0",
+	"shrug_tone4": "9.0",
+	"shrug_tone5": "9.0",
+	"massage": "6.0",
+	"massage_tone1": "8.0",
+	"massage_tone2": "8.0",
+	"massage_tone3": "8.0",
+	"massage_tone4": "8.0",
+	"massage_tone5": "8.0",
+	"haircut": "6.0",
+	"haircut_tone1": "8.0",
+	"haircut_tone2": "8.0",
+	"haircut_tone3": "8.0",
+	"haircut_tone4": "8.0",
+	"haircut_tone5": "8.0",
+	"walking": "6.0",
+	"walking_tone1": "8.0",
+	"walking_tone2": "8.0",
+	"walking_tone3": "8.0",
+	"walking_tone4": "8.0",
+	"walking_tone5": "8.0",
+	"runner": "6.0",
+	"runner_tone1": "8.0",
+	"runner_tone2": "8.0",
+	"runner_tone3": "8.0",
+	"runner_tone4": "8.0",
+	"runner_tone5": "8.0",
+	"dancer": "6.0",
+	"dancer_tone1": "8.0",
+	"dancer_tone2": "8.0",
+	"dancer_tone3": "8.0",
+	"dancer_tone4": "8.0",
+	"dancer_tone5": "8.0",
+	"man_dancing": "9.0",
+	"male_dancer": "9.0",
+	"man_dancing_tone1": "9.0",
+	"male_dancer_tone1": "9.0",
+	"man_dancing_tone2": "9.0",
+	"male_dancer_tone2": "9.0",
+	"man_dancing_tone3": "9.0",
+	"male_dancer_tone3": "9.0",
+	"man_dancing_tone4": "9.0",
+	"male_dancer_tone4": "9.0",
+	"man_dancing_tone5": "9.0",
+	"male_dancer_tone5": "9.0",
+	"dancers": "6.0",
+	"levitate": "7.0",
+	"man_in_business_suit_levitating": "7.0",
+	"speaking_head": "7.0",
+	"speaking_head_in_silhouette": "7.0",
+	"bust_in_silhouette": "6.0",
+	"busts_in_silhouette": "6.0",
+	"fencer": "9.0",
+	"fencing": "9.0",
+	"horse_racing": "6.0",
+	"horse_racing_tone1": "8.0",
+	"horse_racing_tone2": "8.0",
+	"horse_racing_tone3": "8.0",
+	"horse_racing_tone4": "8.0",
+	"horse_racing_tone5": "8.0",
+	"skier": "5.2",
+	"snowboarder": "6.0",
+	"golfer": "7.0",
+	"surfer": "6.0",
+	"surfer_tone1": "8.0",
+	"surfer_tone2": "8.0",
+	"surfer_tone3": "8.0",
+	"surfer_tone4": "8.0",
+	"surfer_tone5": "8.0",
+	"rowboat": "6.0",
+	"rowboat_tone1": "8.0",
+	"rowboat_tone2": "8.0",
+	"rowboat_tone3": "8.0",
+	"rowboat_tone4": "8.0",
+	"rowboat_tone5": "8.0",
+	"swimmer": "6.0",
+	"swimmer_tone1": "8.0",
+	"swimmer_tone2": "8.0",
+	"swimmer_tone3": "8.0",
+	"swimmer_tone4": "8.0",
+	"swimmer_tone5": "8.0",
+	"basketball_player": "5.2",
+	"person_with_ball": "5.2",
+	"basketball_player_tone1": "8.0",
+	"person_with_ball_tone1": "8.0",
+	"basketball_player_tone2": "8.0",
+	"person_with_ball_tone2": "8.0",
+	"basketball_player_tone3": "8.0",
+	"person_with_ball_tone3": "8.0",
+	"basketball_player_tone4": "8.0",
+	"person_with_ball_tone4": "8.0",
+	"basketball_player_tone5": "8.0",
+	"person_with_ball_tone5": "8.0",
+	"lifter": "7.0",
+	"weight_lifter": "7.0",
+	"lifter_tone1": "8.0",
+	"weight_lifter_tone1": "8.0",
+	"lifter_tone2": "8.0",
+	"weight_lifter_tone2": "8.0",
+	"lifter_tone3": "8.0",
+	"weight_lifter_tone3": "8.0",
+	"lifter_tone4": "8.0",
+	"weight_lifter_tone4": "8.0",
+	"lifter_tone5": "8.0",
+	"weight_lifter_tone5": "8.0",
+	"bicyclist": "6.0",
+	"bicyclist_tone1": "8.0",
+	"bicyclist_tone2": "8.0",
+	"bicyclist_tone3": "8.0",
+	"bicyclist_tone4": "8.0",
+	"bicyclist_tone5": "8.0",
+	"mountain_bicyclist": "6.0",
+	"mountain_bicyclist_tone1": "8.0",
+	"mountain_bicyclist_tone2": "8.0",
+	"mountain_bicyclist_tone3": "8.0",
+	"mountain_bicyclist_tone4": "8.0",
+	"mountain_bicyclist_tone5": "8.0",
+	"race_car": "7.0",
+	"racing_car": "7.0",
+	"motorcycle": "7.0",
+	"racing_motorcycle": "7.0",
+	"cartwheel": "9.0",
+	"person_doing_cartwheel": "9.0",
+	"cartwheel_tone1": "9.0",
+	"person_doing_cartwheel_tone1": "9.0",
+	"cartwheel_tone2": "9.0",
+	"person_doing_cartwheel_tone2": "9.0",
+	"cartwheel_tone3": "9.0",
+	"person_doing_cartwheel_tone3": "9.0",
+	"cartwheel_tone4": "9.0",
+	"person_doing_cartwheel_tone4": "9.0",
+	"cartwheel_tone5": "9.0",
+	"person_doing_cartwheel_tone5": "9.0",
+	"wrestlers": "9.0",
+	"wrestling": "9.0",
+	"wrestlers_tone1": "9.0",
+	"wrestling_tone1": "9.0",
+	"wrestlers_tone2": "9.0",
+	"wrestling_tone2": "9.0",
+	"wrestlers_tone3": "9.0",
+	"wrestling_tone3": "9.0",
+	"wrestlers_tone4": "9.0",
+	"wrestling_tone4": "9.0",
+	"wrestlers_tone5": "9.0",
+	"wrestling_tone5": "9.0",
+	"water_polo": "9.0",
+	"water_polo_tone1": "9.0",
+	"water_polo_tone2": "9.0",
+	"water_polo_tone3": "9.0",
+	"water_polo_tone4": "9.0",
+	"water_polo_tone5": "9.0",
+	"handball": "9.0",
+	"handball_tone1": "9.0",
+	"handball_tone2": "9.0",
+	"handball_tone3": "9.0",
+	"handball_tone4": "9.0",
+	"handball_tone5": "9.0",
+	"juggling": "9.0",
+	"juggler": "9.0",
+	"juggling_tone1": "9.0",
+	"juggler_tone1": "9.0",
+	"juggling_tone2": "9.0",
+	"juggler_tone2": "9.0",
+	"juggling_tone3": "9.0",
+	"juggler_tone3": "9.0",
+	"juggling_tone4": "9.0",
+	"juggler_tone4": "9.0",
+	"juggling_tone5": "9.0",
+	"juggler_tone5": "9.0",
+	"couple": "6.0",
+	"two_men_holding_hands": "6.0",
+	"two_women_holding_hands": "6.0",
+	"couplekiss": "6.0",
+	"kiss_mm": "6.0",
+	"couplekiss_mm": "6.0",
+	"kiss_ww": "6.0",
+	"couplekiss_ww": "6.0",
+	"couple_with_heart": "6.0",
+	"couple_mm": "6.0",
+	"couple_with_heart_mm": "6.0",
+	"couple_ww": "6.0",
+	"couple_with_heart_ww": "6.0",
+	"family": "6.0",
+	"family_mwg": "6.0",
+	"family_mwgb": "6.0",
+	"family_mwbb": "6.0",
+	"family_mwgg": "6.0",
+	"family_mmb": "6.0",
+	"family_mmg": "6.0",
+	"family_mmgb": "6.0",
+	"family_mmbb": "6.0",
+	"family_mmgg": "6.0",
+	"family_wwb": "6.0",
+	"family_wwg": "6.0",
+	"family_wwgb": "6.0",
+	"family_wwbb": "6.0",
+	"family_wwgg": "6.0",
+	"tone1": "8.0",
+	"tone2": "8.0",
+	"tone3": "8.0",
+	"tone4": "8.0",
+	"tone5": "8.0",
+	"muscle": "6.0",
+	"muscle_tone1": "8.0",
+	"muscle_tone2": "8.0",
+	"muscle_tone3": "8.0",
+	"muscle_tone4": "8.0",
+	"muscle_tone5": "8.0",
+	"selfie": "9.0",
+	"selfie_tone1": "9.0",
+	"selfie_tone2": "9.0",
+	"selfie_tone3": "9.0",
+	"selfie_tone4": "9.0",
+	"selfie_tone5": "9.0",
+	"point_left": "6.0",
+	"point_left_tone1": "8.0",
+	"point_left_tone2": "8.0",
+	"point_left_tone3": "8.0",
+	"point_left_tone4": "8.0",
+	"point_left_tone5": "8.0",
+	"point_right": "6.0",
+	"point_right_tone1": "8.0",
+	"point_right_tone2": "8.0",
+	"point_right_tone3": "8.0",
+	"point_right_tone4": "8.0",
+	"point_right_tone5": "8.0",
+	"point_up": "1.1",
+	"point_up_tone1": "8.0",
+	"point_up_tone2": "8.0",
+	"point_up_tone3": "8.0",
+	"point_up_tone4": "8.0",
+	"point_up_tone5": "8.0",
+	"point_up_2": "6.0",
+	"point_up_2_tone1": "8.0",
+	"point_up_2_tone2": "8.0",
+	"point_up_2_tone3": "8.0",
+	"point_up_2_tone4": "8.0",
+	"point_up_2_tone5": "8.0",
+	"middle_finger": "7.0",
+	"reversed_hand_with_middle_finger_extended": "7.0",
+	"middle_finger_tone1": "8.0",
+	"reversed_hand_with_middle_finger_extended_tone1": "8.0",
+	"middle_finger_tone2": "8.0",
+	"reversed_hand_with_middle_finger_extended_tone2": "8.0",
+	"middle_finger_tone3": "8.0",
+	"reversed_hand_with_middle_finger_extended_tone3": "8.0",
+	"middle_finger_tone4": "8.0",
+	"reversed_hand_with_middle_finger_extended_tone4": "8.0",
+	"middle_finger_tone5": "8.0",
+	"reversed_hand_with_middle_finger_extended_tone5": "8.0",
+	"point_down": "6.0",
+	"point_down_tone1": "8.0",
+	"point_down_tone2": "8.0",
+	"point_down_tone3": "8.0",
+	"point_down_tone4": "8.0",
+	"point_down_tone5": "8.0",
+	"v": "1.1",
+	"v_tone1": "8.0",
+	"v_tone2": "8.0",
+	"v_tone3": "8.0",
+	"v_tone4": "8.0",
+	"v_tone5": "8.0",
+	"fingers_crossed": "9.0",
+	"hand_with_index_and_middle_finger_crossed": "9.0",
+	"fingers_crossed_tone1": "9.0",
+	"hand_with_index_and_middle_fingers_crossed_tone1": "9.0",
+	"fingers_crossed_tone2": "9.0",
+	"hand_with_index_and_middle_fingers_crossed_tone2": "9.0",
+	"fingers_crossed_tone3": "9.0",
+	"hand_with_index_and_middle_fingers_crossed_tone3": "9.0",
+	"fingers_crossed_tone4": "9.0",
+	"hand_with_index_and_middle_fingers_crossed_tone4": "9.0",
+	"fingers_crossed_tone5": "9.0",
+	"hand_with_index_and_middle_fingers_crossed_tone5": "9.0",
+	"vulcan": "7.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers": "7.0",
+	"vulcan_tone1": "8.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers_tone1": "8.0",
+	"vulcan_tone2": "8.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers_tone2": "8.0",
+	"vulcan_tone3": "8.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers_tone3": "8.0",
+	"vulcan_tone4": "8.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers_tone4": "8.0",
+	"vulcan_tone5": "8.0",
+	"raised_hand_with_part_between_middle_and_ring_fingers_tone5": "8.0",
+	"metal": "8.0",
+	"sign_of_the_horns": "8.0",
+	"metal_tone1": "8.0",
+	"sign_of_the_horns_tone1": "8.0",
+	"metal_tone2": "8.0",
+	"sign_of_the_horns_tone2": "8.0",
+	"metal_tone3": "8.0",
+	"sign_of_the_horns_tone3": "8.0",
+	"metal_tone4": "8.0",
+	"sign_of_the_horns_tone4": "8.0",
+	"metal_tone5": "8.0",
+	"sign_of_the_horns_tone5": "8.0",
+	"call_me": "9.0",
+	"call_me_hand": "9.0",
+	"call_me_tone1": "9.0",
+	"call_me_hand_tone1": "9.0",
+	"call_me_tone2": "9.0",
+	"call_me_hand_tone2": "9.0",
+	"call_me_tone3": "9.0",
+	"call_me_hand_tone3": "9.0",
+	"call_me_tone4": "9.0",
+	"call_me_hand_tone4": "9.0",
+	"call_me_tone5": "9.0",
+	"call_me_hand_tone5": "9.0",
+	"hand_splayed": "7.0",
+	"raised_hand_with_fingers_splayed": "7.0",
+	"hand_splayed_tone1": "8.0",
+	"raised_hand_with_fingers_splayed_tone1": "8.0",
+	"hand_splayed_tone2": "8.0",
+	"raised_hand_with_fingers_splayed_tone2": "8.0",
+	"hand_splayed_tone3": "8.0",
+	"raised_hand_with_fingers_splayed_tone3": "8.0",
+	"hand_splayed_tone4": "8.0",
+	"raised_hand_with_fingers_splayed_tone4": "8.0",
+	"hand_splayed_tone5": "8.0",
+	"raised_hand_with_fingers_splayed_tone5": "8.0",
+	"raised_hand": "6.0",
+	"raised_hand_tone1": "8.0",
+	"raised_hand_tone2": "8.0",
+	"raised_hand_tone3": "8.0",
+	"raised_hand_tone4": "8.0",
+	"raised_hand_tone5": "8.0",
+	"ok_hand": "6.0",
+	"ok_hand_tone1": "8.0",
+	"ok_hand_tone2": "8.0",
+	"ok_hand_tone3": "8.0",
+	"ok_hand_tone4": "8.0",
+	"ok_hand_tone5": "8.0",
+	"thumbsup": "6.0",
+	"+1": "6.0",
+	"thumbup": "6.0",
+	"thumbsup_tone1": "8.0",
+	"+1_tone1": "8.0",
+	"thumbup_tone1": "8.0",
+	"thumbsup_tone2": "8.0",
+	"+1_tone2": "8.0",
+	"thumbup_tone2": "8.0",
+	"thumbsup_tone3": "8.0",
+	"+1_tone3": "8.0",
+	"thumbup_tone3": "8.0",
+	"thumbsup_tone4": "8.0",
+	"+1_tone4": "8.0",
+	"thumbup_tone4": "8.0",
+	"thumbsup_tone5": "8.0",
+	"+1_tone5": "8.0",
+	"thumbup_tone5": "8.0",
+	"thumbsdown": "6.0",
+	"-1": "6.0",
+	"thumbdown": "6.0",
+	"thumbsdown_tone1": "8.0",
+	"-1_tone1": "8.0",
+	"thumbdown_tone1": "8.0",
+	"thumbsdown_tone2": "8.0",
+	"-1_tone2": "8.0",
+	"thumbdown_tone2": "8.0",
+	"thumbsdown_tone3": "8.0",
+	"-1_tone3": "8.0",
+	"thumbdown_tone3": "8.0",
+	"thumbsdown_tone4": "8.0",
+	"-1_tone4": "8.0",
+	"thumbdown_tone4": "8.0",
+	"thumbsdown_tone5": "8.0",
+	"-1_tone5": "8.0",
+	"thumbdown_tone5": "8.0",
+	"fist": "6.0",
+	"fist_tone1": "8.0",
+	"fist_tone2": "8.0",
+	"fist_tone3": "8.0",
+	"fist_tone4": "8.0",
+	"fist_tone5": "8.0",
+	"punch": "6.0",
+	"punch_tone1": "8.0",
+	"punch_tone2": "8.0",
+	"punch_tone3": "8.0",
+	"punch_tone4": "8.0",
+	"punch_tone5": "8.0",
+	"left_facing_fist": "9.0",
+	"left_fist": "9.0",
+	"left_facing_fist_tone1": "9.0",
+	"left_fist_tone1": "9.0",
+	"left_facing_fist_tone2": "9.0",
+	"left_fist_tone2": "9.0",
+	"left_facing_fist_tone3": "9.0",
+	"left_fist_tone3": "9.0",
+	"left_facing_fist_tone4": "9.0",
+	"left_fist_tone4": "9.0",
+	"left_facing_fist_tone5": "9.0",
+	"left_fist_tone5": "9.0",
+	"right_facing_fist": "9.0",
+	"right_fist": "9.0",
+	"right_facing_fist_tone1": "9.0",
+	"right_fist_tone1": "9.0",
+	"right_facing_fist_tone2": "9.0",
+	"right_fist_tone2": "9.0",
+	"right_facing_fist_tone3": "9.0",
+	"right_fist_tone3": "9.0",
+	"right_facing_fist_tone4": "9.0",
+	"right_fist_tone4": "9.0",
+	"right_facing_fist_tone5": "9.0",
+	"right_fist_tone5": "9.0",
+	"raised_back_of_hand": "9.0",
+	"back_of_hand": "9.0",
+	"raised_back_of_hand_tone1": "9.0",
+	"back_of_hand_tone1": "9.0",
+	"raised_back_of_hand_tone2": "9.0",
+	"back_of_hand_tone2": "9.0",
+	"raised_back_of_hand_tone3": "9.0",
+	"back_of_hand_tone3": "9.0",
+	"raised_back_of_hand_tone4": "9.0",
+	"back_of_hand_tone4": "9.0",
+	"raised_back_of_hand_tone5": "9.0",
+	"back_of_hand_tone5": "9.0",
+	"wave": "6.0",
+	"wave_tone1": "8.0",
+	"wave_tone2": "8.0",
+	"wave_tone3": "8.0",
+	"wave_tone4": "8.0",
+	"wave_tone5": "8.0",
+	"clap": "6.0",
+	"clap_tone1": "8.0",
+	"clap_tone2": "8.0",
+	"clap_tone3": "8.0",
+	"clap_tone4": "8.0",
+	"clap_tone5": "8.0",
+	"writing_hand": "1.1",
+	"writing_hand_tone1": "8.0",
+	"writing_hand_tone2": "8.0",
+	"writing_hand_tone3": "8.0",
+	"writing_hand_tone4": "8.0",
+	"writing_hand_tone5": "8.0",
+	"open_hands": "6.0",
+	"open_hands_tone1": "8.0",
+	"open_hands_tone2": "8.0",
+	"open_hands_tone3": "8.0",
+	"open_hands_tone4": "8.0",
+	"open_hands_tone5": "8.0",
+	"raised_hands": "6.0",
+	"raised_hands_tone1": "8.0",
+	"raised_hands_tone2": "8.0",
+	"raised_hands_tone3": "8.0",
+	"raised_hands_tone4": "8.0",
+	"raised_hands_tone5": "8.0",
+	"pray": "6.0",
+	"pray_tone1": "8.0",
+	"pray_tone2": "8.0",
+	"pray_tone3": "8.0",
+	"pray_tone4": "8.0",
+	"pray_tone5": "8.0",
+	"handshake": "9.0",
+	"shaking_hands": "9.0",
+	"handshake_tone1": "9.0",
+	"shaking_hands_tone1": "9.0",
+	"handshake_tone2": "9.0",
+	"shaking_hands_tone2": "9.0",
+	"handshake_tone3": "9.0",
+	"shaking_hands_tone3": "9.0",
+	"handshake_tone4": "9.0",
+	"shaking_hands_tone4": "9.0",
+	"handshake_tone5": "9.0",
+	"shaking_hands_tone5": "9.0",
+	"nail_care": "6.0",
+	"nail_care_tone1": "8.0",
+	"nail_care_tone2": "8.0",
+	"nail_care_tone3": "8.0",
+	"nail_care_tone4": "8.0",
+	"nail_care_tone5": "8.0",
+	"ear": "6.0",
+	"ear_tone1": "8.0",
+	"ear_tone2": "8.0",
+	"ear_tone3": "8.0",
+	"ear_tone4": "8.0",
+	"ear_tone5": "8.0",
+	"nose": "6.0",
+	"nose_tone1": "8.0",
+	"nose_tone2": "8.0",
+	"nose_tone3": "8.0",
+	"nose_tone4": "8.0",
+	"nose_tone5": "8.0",
+	"footprints": "6.0",
+	"eyes": "6.0",
+	"eye": "7.0",
+	"eye_in_speech_bubble": "7.0",
+	"tongue": "6.0",
+	"lips": "6.0",
+	"kiss": "6.0",
+	"cupid": "6.0",
+	"heart": "1.1",
+	"heartbeat": "6.0",
+	"broken_heart": "6.0",
+	"two_hearts": "6.0",
+	"sparkling_heart": "6.0",
+	"heartpulse": "6.0",
+	"blue_heart": "6.0",
+	"green_heart": "6.0",
+	"yellow_heart": "6.0",
+	"purple_heart": "6.0",
+	"black_heart": "9.0",
+	"gift_heart": "6.0",
+	"revolving_hearts": "6.0",
+	"heart_decoration": "6.0",
+	"heart_exclamation": "1.1",
+	"heavy_heart_exclamation_mark_ornament": "1.1",
+	"love_letter": "6.0",
+	"zzz": "6.0",
+	"anger": "6.0",
+	"bomb": "6.0",
+	"boom": "6.0",
+	"sweat_drops": "6.0",
+	"dash": "6.0",
+	"dizzy": "6.0",
+	"speech_balloon": "6.0",
+	"speech_left": "7.0",
+	"left_speech_bubble": "7.0",
+	"anger_right": "7.0",
+	"right_anger_bubble": "7.0",
+	"thought_balloon": "6.0",
+	"hole": "7.0",
+	"eyeglasses": "6.0",
+	"dark_sunglasses": "7.0",
+	"necktie": "6.0",
+	"shirt": "6.0",
+	"jeans": "6.0",
+	"dress": "6.0",
+	"kimono": "6.0",
+	"bikini": "6.0",
+	"womans_clothes": "6.0",
+	"purse": "6.0",
+	"handbag": "6.0",
+	"pouch": "6.0",
+	"shopping_bags": "7.0",
+	"school_satchel": "6.0",
+	"mans_shoe": "6.0",
+	"athletic_shoe": "6.0",
+	"high_heel": "6.0",
+	"sandal": "6.0",
+	"boot": "6.0",
+	"crown": "6.0",
+	"womans_hat": "6.0",
+	"tophat": "6.0",
+	"mortar_board": "6.0",
+	"helmet_with_cross": "5.2",
+	"helmet_with_white_cross": "5.2",
+	"prayer_beads": "8.0",
+	"lipstick": "6.0",
+	"ring": "6.0",
+	"gem": "6.0",
+	"monkey_face": "6.0",
+	"monkey": "6.0",
+	"gorilla": "9.0",
+	"dog": "6.0",
+	"dog2": "6.0",
+	"poodle": "6.0",
+	"wolf": "6.0",
+	"fox": "9.0",
+	"fox_face": "9.0",
+	"cat": "6.0",
+	"cat2": "6.0",
+	"lion_face": "8.0",
+	"lion": "8.0",
+	"tiger": "6.0",
+	"tiger2": "6.0",
+	"leopard": "6.0",
+	"horse": "6.0",
+	"racehorse": "6.0",
+	"deer": "9.0",
+	"unicorn": "8.0",
+	"unicorn_face": "8.0",
+	"cow": "6.0",
+	"ox": "6.0",
+	"water_buffalo": "6.0",
+	"cow2": "6.0",
+	"pig": "6.0",
+	"pig2": "6.0",
+	"boar": "6.0",
+	"pig_nose": "6.0",
+	"ram": "6.0",
+	"sheep": "6.0",
+	"goat": "6.0",
+	"dromedary_camel": "6.0",
+	"camel": "6.0",
+	"elephant": "6.0",
+	"rhino": "9.0",
+	"rhinoceros": "9.0",
+	"mouse": "6.0",
+	"mouse2": "6.0",
+	"rat": "6.0",
+	"hamster": "6.0",
+	"rabbit": "6.0",
+	"rabbit2": "6.0",
+	"chipmunk": "7.0",
+	"bat": "9.0",
+	"bear": "6.0",
+	"koala": "6.0",
+	"panda_face": "6.0",
+	"feet": "6.0",
+	"paw_prints": "6.0",
+	"turkey": "8.0",
+	"chicken": "6.0",
+	"rooster": "6.0",
+	"hatching_chick": "6.0",
+	"baby_chick": "6.0",
+	"hatched_chick": "6.0",
+	"bird": "6.0",
+	"penguin": "6.0",
+	"dove": "7.0",
+	"dove_of_peace": "7.0",
+	"eagle": "9.0",
+	"duck": "9.0",
+	"owl": "9.0",
+	"frog": "6.0",
+	"crocodile": "6.0",
+	"turtle": "6.0",
+	"lizard": "9.0",
+	"snake": "6.0",
+	"dragon_face": "6.0",
+	"dragon": "6.0",
+	"whale": "6.0",
+	"whale2": "6.0",
+	"dolphin": "6.0",
+	"fish": "6.0",
+	"tropical_fish": "6.0",
+	"blowfish": "6.0",
+	"shark": "9.0",
+	"octopus": "6.0",
+	"shell": "6.0",
+	"crab": "8.0",
+	"shrimp": "9.0",
+	"squid": "9.0",
+	"butterfly": "9.0",
+	"snail": "6.0",
+	"bug": "6.0",
+	"ant": "6.0",
+	"bee": "6.0",
+	"beetle": "6.0",
+	"spider": "7.0",
+	"spider_web": "7.0",
+	"scorpion": "8.0",
+	"bouquet": "6.0",
+	"cherry_blossom": "6.0",
+	"white_flower": "6.0",
+	"rosette": "7.0",
+	"rose": "6.0",
+	"wilted_rose": "9.0",
+	"wilted_flower": "9.0",
+	"hibiscus": "6.0",
+	"sunflower": "6.0",
+	"blossom": "6.0",
+	"tulip": "6.0",
+	"seedling": "6.0",
+	"evergreen_tree": "6.0",
+	"deciduous_tree": "6.0",
+	"palm_tree": "6.0",
+	"cactus": "6.0",
+	"ear_of_rice": "6.0",
+	"herb": "6.0",
+	"shamrock": "4.1",
+	"four_leaf_clover": "6.0",
+	"maple_leaf": "6.0",
+	"fallen_leaf": "6.0",
+	"leaves": "6.0",
+	"grapes": "6.0",
+	"melon": "6.0",
+	"watermelon": "6.0",
+	"tangerine": "6.0",
+	"lemon": "6.0",
+	"banana": "6.0",
+	"pineapple": "6.0",
+	"apple": "6.0",
+	"green_apple": "6.0",
+	"pear": "6.0",
+	"peach": "6.0",
+	"cherries": "6.0",
+	"strawberry": "6.0",
+	"kiwi": "9.0",
+	"kiwifruit": "9.0",
+	"tomato": "6.0",
+	"avocado": "9.0",
+	"eggplant": "6.0",
+	"potato": "9.0",
+	"carrot": "9.0",
+	"corn": "6.0",
+	"hot_pepper": "7.0",
+	"cucumber": "9.0",
+	"mushroom": "6.0",
+	"peanuts": "9.0",
+	"shelled_peanut": "9.0",
+	"chestnut": "6.0",
+	"bread": "6.0",
+	"croissant": "9.0",
+	"french_bread": "9.0",
+	"baguette_bread": "9.0",
+	"pancakes": "9.0",
+	"cheese": "8.0",
+	"cheese_wedge": "8.0",
+	"meat_on_bone": "6.0",
+	"poultry_leg": "6.0",
+	"bacon": "9.0",
+	"hamburger": "6.0",
+	"fries": "6.0",
+	"pizza": "6.0",
+	"hotdog": "8.0",
+	"hot_dog": "8.0",
+	"taco": "8.0",
+	"burrito": "8.0",
+	"stuffed_flatbread": "9.0",
+	"stuffed_pita": "9.0",
+	"egg": "9.0",
+	"cooking": "6.0",
+	"shallow_pan_of_food": "9.0",
+	"paella": "9.0",
+	"stew": "6.0",
+	"salad": "9.0",
+	"green_salad": "9.0",
+	"popcorn": "8.0",
+	"bento": "6.0",
+	"rice_cracker": "6.0",
+	"rice_ball": "6.0",
+	"rice": "6.0",
+	"curry": "6.0",
+	"ramen": "6.0",
+	"spaghetti": "6.0",
+	"sweet_potato": "6.0",
+	"oden": "6.0",
+	"sushi": "6.0",
+	"fried_shrimp": "6.0",
+	"fish_cake": "6.0",
+	"dango": "6.0",
+	"icecream": "6.0",
+	"shaved_ice": "6.0",
+	"ice_cream": "6.0",
+	"doughnut": "6.0",
+	"cookie": "6.0",
+	"birthday": "6.0",
+	"cake": "6.0",
+	"chocolate_bar": "6.0",
+	"candy": "6.0",
+	"lollipop": "6.0",
+	"custard": "6.0",
+	"pudding": "6.0",
+	"flan": "6.0",
+	"honey_pot": "6.0",
+	"baby_bottle": "6.0",
+	"milk": "9.0",
+	"glass_of_milk": "9.0",
+	"coffee": "4.0",
+	"tea": "6.0",
+	"sake": "6.0",
+	"champagne": "8.0",
+	"bottle_with_popping_cork": "8.0",
+	"wine_glass": "6.0",
+	"cocktail": "6.0",
+	"tropical_drink": "6.0",
+	"beer": "6.0",
+	"beers": "6.0",
+	"champagne_glass": "9.0",
+	"clinking_glass": "9.0",
+	"tumbler_glass": "9.0",
+	"whisky": "9.0",
+	"fork_knife_plate": "7.0",
+	"fork_and_knife_with_plate": "7.0",
+	"fork_and_knife": "6.0",
+	"spoon": "9.0",
+	"knife": "6.0",
+	"amphora": "8.0",
+	"earth_africa": "6.0",
+	"earth_americas": "6.0",
+	"earth_asia": "6.0",
+	"globe_with_meridians": "6.0",
+	"map": "7.0",
+	"world_map": "7.0",
+	"japan": "6.0",
+	"mountain_snow": "7.0",
+	"snow_capped_mountain": "7.0",
+	"mountain": "5.2",
+	"volcano": "6.0",
+	"mount_fuji": "6.0",
+	"camping": "7.0",
+	"beach": "7.0",
+	"beach_with_umbrella": "7.0",
+	"desert": "7.0",
+	"island": "7.0",
+	"desert_island": "7.0",
+	"park": "7.0",
+	"national_park": "7.0",
+	"stadium": "7.0",
+	"classical_building": "7.0",
+	"construction_site": "7.0",
+	"building_construction": "7.0",
+	"homes": "7.0",
+	"house_buildings": "7.0",
+	"cityscape": "7.0",
+	"house_abandoned": "7.0",
+	"derelict_house_building": "7.0",
+	"house": "6.0",
+	"house_with_garden": "6.0",
+	"office": "6.0",
+	"post_office": "6.0",
+	"european_post_office": "6.0",
+	"hospital": "6.0",
+	"bank": "6.0",
+	"hotel": "6.0",
+	"love_hotel": "6.0",
+	"convenience_store": "6.0",
+	"school": "6.0",
+	"department_store": "6.0",
+	"factory": "6.0",
+	"japanese_castle": "6.0",
+	"european_castle": "6.0",
+	"wedding": "6.0",
+	"tokyo_tower": "6.0",
+	"statue_of_liberty": "6.0",
+	"church": "5.2",
+	"mosque": "8.0",
+	"synagogue": "8.0",
+	"shinto_shrine": "5.2",
+	"kaaba": "8.0",
+	"fountain": "5.2",
+	"tent": "5.2",
+	"foggy": "6.0",
+	"night_with_stars": "6.0",
+	"sunrise_over_mountains": "6.0",
+	"sunrise": "6.0",
+	"city_dusk": "6.0",
+	"city_sunset": "6.0",
+	"city_sunrise": "6.0",
+	"bridge_at_night": "6.0",
+	"hotsprings": "1.1",
+	"milky_way": "6.0",
+	"carousel_horse": "6.0",
+	"ferris_wheel": "6.0",
+	"roller_coaster": "6.0",
+	"barber": "6.0",
+	"circus_tent": "6.0",
+	"performing_arts": "6.0",
+	"frame_photo": "7.0",
+	"frame_with_picture": "7.0",
+	"art": "6.0",
+	"slot_machine": "6.0",
+	"steam_locomotive": "6.0",
+	"railway_car": "6.0",
+	"bullettrain_side": "6.0",
+	"bullettrain_front": "6.0",
+	"train2": "6.0",
+	"metro": "6.0",
+	"light_rail": "6.0",
+	"station": "6.0",
+	"tram": "6.0",
+	"monorail": "6.0",
+	"mountain_railway": "6.0",
+	"train": "6.0",
+	"bus": "6.0",
+	"oncoming_bus": "6.0",
+	"trolleybus": "6.0",
+	"minibus": "6.0",
+	"ambulance": "6.0",
+	"fire_engine": "6.0",
+	"police_car": "6.0",
+	"oncoming_police_car": "6.0",
+	"taxi": "6.0",
+	"oncoming_taxi": "6.0",
+	"red_car": "6.0",
+	"oncoming_automobile": "6.0",
+	"blue_car": "6.0",
+	"truck": "6.0",
+	"articulated_lorry": "6.0",
+	"tractor": "6.0",
+	"bike": "6.0",
+	"scooter": "9.0",
+	"motor_scooter": "9.0",
+	"motorbike": "9.0",
+	"busstop": "6.0",
+	"motorway": "7.0",
+	"railway_track": "7.0",
+	"railroad_track": "7.0",
+	"fuelpump": "5.2",
+	"rotating_light": "6.0",
+	"traffic_light": "6.0",
+	"vertical_traffic_light": "6.0",
+	"construction": "6.0",
+	"octagonal_sign": "9.0",
+	"stop_sign": "9.0",
+	"anchor": "4.1",
+	"sailboat": "5.2",
+	"canoe": "9.0",
+	"kayak": "9.0",
+	"speedboat": "6.0",
+	"cruise_ship": "7.0",
+	"passenger_ship": "7.0",
+	"ferry": "5.2",
+	"motorboat": "7.0",
+	"ship": "6.0",
+	"airplane": "1.1",
+	"airplane_small": "7.0",
+	"small_airplane": "7.0",
+	"airplane_departure": "7.0",
+	"airplane_arriving": "7.0",
+	"seat": "6.0",
+	"helicopter": "6.0",
+	"suspension_railway": "6.0",
+	"mountain_cableway": "6.0",
+	"aerial_tramway": "6.0",
+	"rocket": "6.0",
+	"satellite_orbital": "7.0",
+	"bellhop": "7.0",
+	"bellhop_bell": "7.0",
+	"door": "6.0",
+	"sleeping_accommodation": "7.0",
+	"bed": "7.0",
+	"couch": "7.0",
+	"couch_and_lamp": "7.0",
+	"toilet": "6.0",
+	"shower": "6.0",
+	"bath": "6.0",
+	"bath_tone1": "8.0",
+	"bath_tone2": "8.0",
+	"bath_tone3": "8.0",
+	"bath_tone4": "8.0",
+	"bath_tone5": "8.0",
+	"bathtub": "6.0",
+	"hourglass": "1.1",
+	"hourglass_flowing_sand": "6.0",
+	"watch": "1.1",
+	"alarm_clock": "6.0",
+	"stopwatch": "6.0",
+	"timer": "6.0",
+	"timer_clock": "6.0",
+	"clock": "7.0",
+	"mantlepiece_clock": "7.0",
+	"clock12": "6.0",
+	"clock1230": "6.0",
+	"clock1": "6.0",
+	"clock130": "6.0",
+	"clock2": "6.0",
+	"clock230": "6.0",
+	"clock3": "6.0",
+	"clock330": "6.0",
+	"clock4": "6.0",
+	"clock430": "6.0",
+	"clock5": "6.0",
+	"clock530": "6.0",
+	"clock6": "6.0",
+	"clock630": "6.0",
+	"clock7": "6.0",
+	"clock730": "6.0",
+	"clock8": "6.0",
+	"clock830": "6.0",
+	"clock9": "6.0",
+	"clock930": "6.0",
+	"clock10": "6.0",
+	"clock1030": "6.0",
+	"clock11": "6.0",
+	"clock1130": "6.0",
+	"new_moon": "6.0",
+	"waxing_crescent_moon": "6.0",
+	"first_quarter_moon": "6.0",
+	"waxing_gibbous_moon": "6.0",
+	"full_moon": "6.0",
+	"waning_gibbous_moon": "6.0",
+	"last_quarter_moon": "6.0",
+	"waning_crescent_moon": "6.0",
+	"crescent_moon": "6.0",
+	"new_moon_with_face": "6.0",
+	"first_quarter_moon_with_face": "6.0",
+	"last_quarter_moon_with_face": "6.0",
+	"thermometer": "7.0",
+	"sunny": "1.1",
+	"full_moon_with_face": "6.0",
+	"sun_with_face": "6.0",
+	"star": "5.1",
+	"star2": "6.0",
+	"stars": "6.0",
+	"cloud": "1.1",
+	"partly_sunny": "5.2",
+	"thunder_cloud_rain": "5.2",
+	"thunder_cloud_and_rain": "5.2",
+	"white_sun_small_cloud": "7.0",
+	"white_sun_with_small_cloud": "7.0",
+	"white_sun_cloud": "7.0",
+	"white_sun_behind_cloud": "7.0",
+	"white_sun_rain_cloud": "7.0",
+	"white_sun_behind_cloud_with_rain": "7.0",
+	"cloud_rain": "7.0",
+	"cloud_with_rain": "7.0",
+	"cloud_snow": "7.0",
+	"cloud_with_snow": "7.0",
+	"cloud_lightning": "7.0",
+	"cloud_with_lightning": "7.0",
+	"cloud_tornado": "7.0",
+	"cloud_with_tornado": "7.0",
+	"fog": "7.0",
+	"wind_blowing_face": "7.0",
+	"cyclone": "6.0",
+	"rainbow": "6.0",
+	"closed_umbrella": "6.0",
+	"umbrella2": "1.1",
+	"umbrella": "4.0",
+	"beach_umbrella": "5.2",
+	"umbrella_on_ground": "5.2",
+	"zap": "4.0",
+	"snowflake": "1.1",
+	"snowman2": "1.1",
+	"snowman": "5.2",
+	"comet": "1.1",
+	"fire": "6.0",
+	"flame": "6.0",
+	"droplet": "6.0",
+	"ocean": "6.0",
+	"jack_o_lantern": "6.0",
+	"christmas_tree": "6.0",
+	"fireworks": "6.0",
+	"sparkler": "6.0",
+	"sparkles": "6.0",
+	"balloon": "6.0",
+	"tada": "6.0",
+	"confetti_ball": "6.0",
+	"tanabata_tree": "6.0",
+	"bamboo": "6.0",
+	"dolls": "6.0",
+	"flags": "6.0",
+	"wind_chime": "6.0",
+	"rice_scene": "6.0",
+	"ribbon": "6.0",
+	"gift": "6.0",
+	"reminder_ribbon": "7.0",
+	"tickets": "7.0",
+	"admission_tickets": "7.0",
+	"ticket": "6.0",
+	"military_medal": "7.0",
+	"trophy": "6.0",
+	"medal": "7.0",
+	"sports_medal": "7.0",
+	"first_place": "9.0",
+	"first_place_medal": "9.0",
+	"second_place": "9.0",
+	"second_place_medal": "9.0",
+	"third_place": "9.0",
+	"third_place_medal": "9.0",
+	"soccer": "5.2",
+	"baseball": "5.2",
+	"basketball": "6.0",
+	"volleyball": "8.0",
+	"football": "6.0",
+	"rugby_football": "6.0",
+	"tennis": "6.0",
+	"8ball": "6.0",
+	"bowling": "6.0",
+	"cricket": "8.0",
+	"cricket_bat_ball": "8.0",
+	"field_hockey": "8.0",
+	"hockey": "8.0",
+	"ping_pong": "8.0",
+	"table_tennis": "8.0",
+	"badminton": "8.0",
+	"boxing_glove": "9.0",
+	"boxing_gloves": "9.0",
+	"martial_arts_uniform": "9.0",
+	"karate_uniform": "9.0",
+	"goal": "9.0",
+	"goal_net": "9.0",
+	"dart": "6.0",
+	"golf": "5.2",
+	"ice_skate": "5.2",
+	"fishing_pole_and_fish": "6.0",
+	"running_shirt_with_sash": "6.0",
+	"ski": "6.0",
+	"video_game": "6.0",
+	"joystick": "7.0",
+	"game_die": "6.0",
+	"spades": "1.1",
+	"hearts": "1.1",
+	"diamonds": "1.1",
+	"clubs": "1.1",
+	"black_joker": "6.0",
+	"mahjong": "5.1",
+	"flower_playing_cards": "6.0",
+	"mute": "6.0",
+	"speaker": "6.0",
+	"sound": "6.0",
+	"loud_sound": "6.0",
+	"loudspeaker": "6.0",
+	"mega": "6.0",
+	"postal_horn": "6.0",
+	"bell": "6.0",
+	"no_bell": "6.0",
+	"musical_score": "6.0",
+	"musical_note": "6.0",
+	"notes": "6.0",
+	"microphone2": "7.0",
+	"studio_microphone": "7.0",
+	"level_slider": "7.0",
+	"control_knobs": "7.0",
+	"microphone": "6.0",
+	"headphones": "6.0",
+	"radio": "6.0",
+	"saxophone": "6.0",
+	"guitar": "6.0",
+	"musical_keyboard": "6.0",
+	"trumpet": "6.0",
+	"violin": "6.0",
+	"drum": "9.0",
+	"drum_with_drumsticks": "9.0",
+	"iphone": "6.0",
+	"calling": "6.0",
+	"telephone": "1.1",
+	"telephone_receiver": "6.0",
+	"pager": "6.0",
+	"fax": "6.0",
+	"battery": "6.0",
+	"electric_plug": "6.0",
+	"computer": "6.0",
+	"desktop": "7.0",
+	"desktop_computer": "7.0",
+	"printer": "7.0",
+	"keyboard": "1.1",
+	"mouse_three_button": "7.0",
+	"three_button_mouse": "7.0",
+	"trackball": "7.0",
+	"minidisc": "6.0",
+	"floppy_disk": "6.0",
+	"cd": "6.0",
+	"dvd": "6.0",
+	"movie_camera": "6.0",
+	"film_frames": "7.0",
+	"projector": "7.0",
+	"film_projector": "7.0",
+	"clapper": "6.0",
+	"tv": "6.0",
+	"camera": "6.0",
+	"camera_with_flash": "7.0",
+	"video_camera": "6.0",
+	"vhs": "6.0",
+	"mag": "6.0",
+	"mag_right": "6.0",
+	"microscope": "6.0",
+	"telescope": "6.0",
+	"satellite": "6.0",
+	"candle": "7.0",
+	"bulb": "6.0",
+	"flashlight": "6.0",
+	"izakaya_lantern": "6.0",
+	"notebook_with_decorative_cover": "6.0",
+	"closed_book": "6.0",
+	"book": "6.0",
+	"green_book": "6.0",
+	"blue_book": "6.0",
+	"orange_book": "6.0",
+	"books": "6.0",
+	"notebook": "6.0",
+	"ledger": "6.0",
+	"page_with_curl": "6.0",
+	"scroll": "6.0",
+	"page_facing_up": "6.0",
+	"newspaper": "6.0",
+	"newspaper2": "7.0",
+	"rolled_up_newspaper": "7.0",
+	"bookmark_tabs": "6.0",
+	"bookmark": "6.0",
+	"label": "7.0",
+	"moneybag": "6.0",
+	"yen": "6.0",
+	"dollar": "6.0",
+	"euro": "6.0",
+	"pound": "6.0",
+	"money_with_wings": "6.0",
+	"credit_card": "6.0",
+	"chart": "6.0",
+	"currency_exchange": "6.0",
+	"heavy_dollar_sign": "6.0",
+	"envelope": "1.1",
+	"e-mail": "6.0",
+	"email": "6.0",
+	"incoming_envelope": "6.0",
+	"envelope_with_arrow": "6.0",
+	"outbox_tray": "6.0",
+	"inbox_tray": "6.0",
+	"package": "6.0",
+	"mailbox": "6.0",
+	"mailbox_closed": "6.0",
+	"mailbox_with_mail": "6.0",
+	"mailbox_with_no_mail": "6.0",
+	"postbox": "6.0",
+	"ballot_box": "7.0",
+	"ballot_box_with_ballot": "7.0",
+	"pencil2": "1.1",
+	"black_nib": "1.1",
+	"pen_fountain": "7.0",
+	"lower_left_fountain_pen": "7.0",
+	"pen_ballpoint": "7.0",
+	"lower_left_ballpoint_pen": "7.0",
+	"paintbrush": "7.0",
+	"lower_left_paintbrush": "7.0",
+	"crayon": "7.0",
+	"lower_left_crayon": "7.0",
+	"pencil": "6.0",
+	"briefcase": "6.0",
+	"file_folder": "6.0",
+	"open_file_folder": "6.0",
+	"dividers": "7.0",
+	"card_index_dividers": "7.0",
+	"date": "6.0",
+	"calendar": "6.0",
+	"notepad_spiral": "7.0",
+	"spiral_note_pad": "7.0",
+	"calendar_spiral": "7.0",
+	"spiral_calendar_pad": "7.0",
+	"card_index": "6.0",
+	"chart_with_upwards_trend": "6.0",
+	"chart_with_downwards_trend": "6.0",
+	"bar_chart": "6.0",
+	"clipboard": "6.0",
+	"pushpin": "6.0",
+	"round_pushpin": "6.0",
+	"paperclip": "6.0",
+	"paperclips": "7.0",
+	"linked_paperclips": "7.0",
+	"straight_ruler": "6.0",
+	"triangular_ruler": "6.0",
+	"scissors": "1.1",
+	"card_box": "7.0",
+	"card_file_box": "7.0",
+	"file_cabinet": "7.0",
+	"wastebasket": "7.0",
+	"lock": "6.0",
+	"unlock": "6.0",
+	"lock_with_ink_pen": "6.0",
+	"closed_lock_with_key": "6.0",
+	"key": "6.0",
+	"key2": "7.0",
+	"old_key": "7.0",
+	"hammer": "6.0",
+	"pick": "5.2",
+	"hammer_pick": "4.1",
+	"hammer_and_pick": "4.1",
+	"tools": "7.0",
+	"hammer_and_wrench": "7.0",
+	"dagger": "7.0",
+	"dagger_knife": "7.0",
+	"crossed_swords": "4.1",
+	"gun": "6.0",
+	"bow_and_arrow": "8.0",
+	"archery": "8.0",
+	"shield": "7.0",
+	"wrench": "6.0",
+	"nut_and_bolt": "6.0",
+	"gear": "4.1",
+	"compression": "7.0",
+	"alembic": "4.1",
+	"scales": "4.1",
+	"link": "6.0",
+	"chains": "5.2",
+	"syringe": "6.0",
+	"pill": "6.0",
+	"smoking": "6.0",
+	"coffin": "4.1",
+	"urn": "4.1",
+	"funeral_urn": "4.1",
+	"moyai": "6.0",
+	"oil": "7.0",
+	"oil_drum": "7.0",
+	"crystal_ball": "6.0",
+	"shopping_cart": "9.0",
+	"shopping_trolley": "9.0",
+	"atm": "6.0",
+	"put_litter_in_its_place": "6.0",
+	"potable_water": "6.0",
+	"wheelchair": "4.1",
+	"mens": "6.0",
+	"womens": "6.0",
+	"restroom": "6.0",
+	"baby_symbol": "6.0",
+	"wc": "6.0",
+	"passport_control": "6.0",
+	"customs": "6.0",
+	"baggage_claim": "6.0",
+	"left_luggage": "6.0",
+	"warning": "4.0",
+	"children_crossing": "6.0",
+	"no_entry": "5.2",
+	"no_entry_sign": "6.0",
+	"no_bicycles": "6.0",
+	"no_smoking": "6.0",
+	"do_not_litter": "6.0",
+	"non-potable_water": "6.0",
+	"no_pedestrians": "6.0",
+	"no_mobile_phones": "6.0",
+	"underage": "6.0",
+	"radioactive": "1.1",
+	"radioactive_sign": "1.1",
+	"biohazard": "1.1",
+	"biohazard_sign": "1.1",
+	"arrow_up": "4.0",
+	"arrow_upper_right": "1.1",
+	"arrow_right": "1.1",
+	"arrow_lower_right": "1.1",
+	"arrow_down": "4.0",
+	"arrow_lower_left": "1.1",
+	"arrow_left": "4.0",
+	"arrow_upper_left": "1.1",
+	"arrow_up_down": "1.1",
+	"left_right_arrow": "1.1",
+	"leftwards_arrow_with_hook": "1.1",
+	"arrow_right_hook": "1.1",
+	"arrow_heading_up": "3.2",
+	"arrow_heading_down": "3.2",
+	"arrows_clockwise": "6.0",
+	"arrows_counterclockwise": "6.0",
+	"back": "6.0",
+	"end": "6.0",
+	"on": "6.0",
+	"soon": "6.0",
+	"top": "6.0",
+	"place_of_worship": "8.0",
+	"worship_symbol": "8.0",
+	"atom": "4.1",
+	"atom_symbol": "4.1",
+	"om_symbol": "7.0",
+	"star_of_david": "1.1",
+	"wheel_of_dharma": "1.1",
+	"yin_yang": "1.1",
+	"cross": "1.1",
+	"latin_cross": "1.1",
+	"orthodox_cross": "1.1",
+	"star_and_crescent": "1.1",
+	"peace": "1.1",
+	"peace_symbol": "1.1",
+	"menorah": "8.0",
+	"six_pointed_star": "6.0",
+	"aries": "1.1",
+	"taurus": "1.1",
+	"gemini": "1.1",
+	"cancer": "1.1",
+	"leo": "1.1",
+	"virgo": "1.1",
+	"libra": "1.1",
+	"scorpius": "1.1",
+	"sagittarius": "1.1",
+	"capricorn": "1.1",
+	"aquarius": "1.1",
+	"pisces": "1.1",
+	"ophiuchus": "6.0",
+	"twisted_rightwards_arrows": "6.0",
+	"repeat": "6.0",
+	"repeat_one": "6.0",
+	"arrow_forward": "1.1",
+	"fast_forward": "6.0",
+	"track_next": "6.0",
+	"next_track": "6.0",
+	"play_pause": "6.0",
+	"arrow_backward": "1.1",
+	"rewind": "6.0",
+	"track_previous": "6.0",
+	"previous_track": "6.0",
+	"arrow_up_small": "6.0",
+	"arrow_double_up": "6.0",
+	"arrow_down_small": "6.0",
+	"arrow_double_down": "6.0",
+	"pause_button": "7.0",
+	"double_vertical_bar": "7.0",
+	"stop_button": "7.0",
+	"record_button": "7.0",
+	"eject": "4.0",
+	"eject_symbol": "4.0",
+	"cinema": "6.0",
+	"low_brightness": "6.0",
+	"high_brightness": "6.0",
+	"signal_strength": "6.0",
+	"vibration_mode": "6.0",
+	"mobile_phone_off": "6.0",
+	"recycle": "3.2",
+	"name_badge": "6.0",
+	"fleur-de-lis": "4.1",
+	"beginner": "6.0",
+	"trident": "6.0",
+	"o": "5.2",
+	"white_check_mark": "6.0",
+	"ballot_box_with_check": "1.1",
+	"heavy_check_mark": "1.1",
+	"heavy_multiplication_x": "1.1",
+	"x": "6.0",
+	"negative_squared_cross_mark": "6.0",
+	"heavy_plus_sign": "6.0",
+	"heavy_minus_sign": "6.0",
+	"heavy_division_sign": "6.0",
+	"curly_loop": "6.0",
+	"loop": "6.0",
+	"part_alternation_mark": "3.2",
+	"eight_spoked_asterisk": "1.1",
+	"eight_pointed_black_star": "1.1",
+	"sparkle": "1.1",
+	"bangbang": "1.1",
+	"interrobang": "3.0",
+	"question": "6.0",
+	"grey_question": "6.0",
+	"grey_exclamation": "6.0",
+	"exclamation": "5.2",
+	"wavy_dash": "1.1",
+	"copyright": "1.1",
+	"registered": "1.1",
+	"tm": "1.1",
+	"hash": "3.0",
+	"asterisk": "3.0",
+	"keycap_asterisk": "3.0",
+	"zero": "3.0",
+	"one": "3.0",
+	"two": "3.0",
+	"three": "3.0",
+	"four": "3.0",
+	"five": "3.0",
+	"six": "3.0",
+	"seven": "3.0",
+	"eight": "3.0",
+	"nine": "3.0",
+	"keycap_ten": "6.0",
+	"capital_abcd": "6.0",
+	"abcd": "6.0",
+	"symbols": "6.0",
+	"abc": "6.0",
+	"a": "6.0",
+	"ab": "6.0",
+	"b": "6.0",
+	"cl": "6.0",
+	"cool": "6.0",
+	"free": "6.0",
+	"information_source": "3.0",
+	"id": "6.0",
+	"m": "1.1",
+	"new": "6.0",
+	"ng": "6.0",
+	"o2": "6.0",
+	"ok": "6.0",
+	"parking": "5.2",
+	"sos": "6.0",
+	"up": "6.0",
+	"vs": "6.0",
+	"koko": "6.0",
+	"sa": "6.0",
+	"u6708": "6.0",
+	"u6709": "6.0",
+	"u6307": "5.2",
+	"ideograph_advantage": "6.0",
+	"u5272": "6.0",
+	"u7121": "5.2",
+	"u7981": "6.0",
+	"accept": "6.0",
+	"u7533": "6.0",
+	"u5408": "6.0",
+	"u7a7a": "6.0",
+	"congratulations": "1.1",
+	"secret": "1.1",
+	"u55b6": "6.0",
+	"u6e80": "6.0",
+	"black_small_square": "1.1",
+	"white_small_square": "1.1",
+	"white_medium_square": "3.2",
+	"black_medium_square": "3.2",
+	"white_medium_small_square": "3.2",
+	"black_medium_small_square": "3.2",
+	"black_large_square": "5.1",
+	"white_large_square": "5.1",
+	"large_orange_diamond": "6.0",
+	"large_blue_diamond": "6.0",
+	"small_orange_diamond": "6.0",
+	"small_blue_diamond": "6.0",
+	"small_red_triangle": "6.0",
+	"small_red_triangle_down": "6.0",
+	"diamond_shape_with_a_dot_inside": "6.0",
+	"radio_button": "6.0",
+	"black_square_button": "6.0",
+	"white_square_button": "6.0",
+	"white_circle": "4.1",
+	"black_circle": "4.1",
+	"red_circle": "6.0",
+	"blue_circle": "6.0",
+	"checkered_flag": "6.0",
+	"triangular_flag_on_post": "6.0",
+	"crossed_flags": "6.0",
+	"flag_black": "6.0",
+	"waving_black_flag": "6.0",
+	"flag_white": "6.0",
+	"waving_white_flag": "6.0",
+	"rainbow_flag": "6.0",
+	"gay_pride_flag": "6.0",
+	"flag_ac": "6.0",
+	"ac": "6.0",
+	"flag_ad": "6.0",
+	"ad": "6.0",
+	"flag_ae": "6.0",
+	"ae": "6.0",
+	"flag_af": "6.0",
+	"af": "6.0",
+	"flag_ag": "6.0",
+	"ag": "6.0",
+	"flag_ai": "6.0",
+	"ai": "6.0",
+	"flag_al": "6.0",
+	"al": "6.0",
+	"flag_am": "6.0",
+	"am": "6.0",
+	"flag_ao": "6.0",
+	"ao": "6.0",
+	"flag_aq": "6.0",
+	"aq": "6.0",
+	"flag_ar": "6.0",
+	"ar": "6.0",
+	"flag_as": "6.0",
+	"as": "6.0",
+	"flag_at": "6.0",
+	"at": "6.0",
+	"flag_au": "6.0",
+	"au": "6.0",
+	"flag_aw": "6.0",
+	"aw": "6.0",
+	"flag_ax": "6.0",
+	"ax": "6.0",
+	"flag_az": "6.0",
+	"az": "6.0",
+	"flag_ba": "6.0",
+	"ba": "6.0",
+	"flag_bb": "6.0",
+	"bb": "6.0",
+	"flag_bd": "6.0",
+	"bd": "6.0",
+	"flag_be": "6.0",
+	"be": "6.0",
+	"flag_bf": "6.0",
+	"bf": "6.0",
+	"flag_bg": "6.0",
+	"bg": "6.0",
+	"flag_bh": "6.0",
+	"bh": "6.0",
+	"flag_bi": "6.0",
+	"bi": "6.0",
+	"flag_bj": "6.0",
+	"bj": "6.0",
+	"flag_bl": "6.0",
+	"bl": "6.0",
+	"flag_bm": "6.0",
+	"bm": "6.0",
+	"flag_bn": "6.0",
+	"bn": "6.0",
+	"flag_bo": "6.0",
+	"bo": "6.0",
+	"flag_bq": "6.0",
+	"bq": "6.0",
+	"flag_br": "6.0",
+	"br": "6.0",
+	"flag_bs": "6.0",
+	"bs": "6.0",
+	"flag_bt": "6.0",
+	"bt": "6.0",
+	"flag_bv": "6.0",
+	"bv": "6.0",
+	"flag_bw": "6.0",
+	"bw": "6.0",
+	"flag_by": "6.0",
+	"by": "6.0",
+	"flag_bz": "6.0",
+	"bz": "6.0",
+	"flag_ca": "6.0",
+	"ca": "6.0",
+	"flag_cc": "6.0",
+	"cc": "6.0",
+	"flag_cd": "6.0",
+	"congo": "6.0",
+	"flag_cf": "6.0",
+	"cf": "6.0",
+	"flag_cg": "6.0",
+	"cg": "6.0",
+	"flag_ch": "6.0",
+	"ch": "6.0",
+	"flag_ci": "6.0",
+	"ci": "6.0",
+	"flag_ck": "6.0",
+	"ck": "6.0",
+	"flag_cl": "6.0",
+	"chile": "6.0",
+	"flag_cm": "6.0",
+	"cm": "6.0",
+	"flag_cn": "6.0",
+	"cn": "6.0",
+	"flag_co": "6.0",
+	"co": "6.0",
+	"flag_cp": "6.0",
+	"cp": "6.0",
+	"flag_cr": "6.0",
+	"cr": "6.0",
+	"flag_cu": "6.0",
+	"cu": "6.0",
+	"flag_cv": "6.0",
+	"cv": "6.0",
+	"flag_cw": "6.0",
+	"cw": "6.0",
+	"flag_cx": "6.0",
+	"cx": "6.0",
+	"flag_cy": "6.0",
+	"cy": "6.0",
+	"flag_cz": "6.0",
+	"cz": "6.0",
+	"flag_de": "6.0",
+	"de": "6.0",
+	"flag_dg": "6.0",
+	"dg": "6.0",
+	"flag_dj": "6.0",
+	"dj": "6.0",
+	"flag_dk": "6.0",
+	"dk": "6.0",
+	"flag_dm": "6.0",
+	"dm": "6.0",
+	"flag_do": "6.0",
+	"do": "6.0",
+	"flag_dz": "6.0",
+	"dz": "6.0",
+	"flag_ea": "6.0",
+	"ea": "6.0",
+	"flag_ec": "6.0",
+	"ec": "6.0",
+	"flag_ee": "6.0",
+	"ee": "6.0",
+	"flag_eg": "6.0",
+	"eg": "6.0",
+	"flag_eh": "6.0",
+	"eh": "6.0",
+	"flag_er": "6.0",
+	"er": "6.0",
+	"flag_es": "6.0",
+	"es": "6.0",
+	"flag_et": "6.0",
+	"et": "6.0",
+	"flag_eu": "6.0",
+	"eu": "6.0",
+	"flag_fi": "6.0",
+	"fi": "6.0",
+	"flag_fj": "6.0",
+	"fj": "6.0",
+	"flag_fk": "6.0",
+	"fk": "6.0",
+	"flag_fm": "6.0",
+	"fm": "6.0",
+	"flag_fo": "6.0",
+	"fo": "6.0",
+	"flag_fr": "6.0",
+	"fr": "6.0",
+	"flag_ga": "6.0",
+	"ga": "6.0",
+	"flag_gb": "6.0",
+	"gb": "6.0",
+	"flag_gd": "6.0",
+	"gd": "6.0",
+	"flag_ge": "6.0",
+	"ge": "6.0",
+	"flag_gf": "6.0",
+	"gf": "6.0",
+	"flag_gg": "6.0",
+	"gg": "6.0",
+	"flag_gh": "6.0",
+	"gh": "6.0",
+	"flag_gi": "6.0",
+	"gi": "6.0",
+	"flag_gl": "6.0",
+	"gl": "6.0",
+	"flag_gm": "6.0",
+	"gm": "6.0",
+	"flag_gn": "6.0",
+	"gn": "6.0",
+	"flag_gp": "6.0",
+	"gp": "6.0",
+	"flag_gq": "6.0",
+	"gq": "6.0",
+	"flag_gr": "6.0",
+	"gr": "6.0",
+	"flag_gs": "6.0",
+	"gs": "6.0",
+	"flag_gt": "6.0",
+	"gt": "6.0",
+	"flag_gu": "6.0",
+	"gu": "6.0",
+	"flag_gw": "6.0",
+	"gw": "6.0",
+	"flag_gy": "6.0",
+	"gy": "6.0",
+	"flag_hk": "6.0",
+	"hk": "6.0",
+	"flag_hm": "6.0",
+	"hm": "6.0",
+	"flag_hn": "6.0",
+	"hn": "6.0",
+	"flag_hr": "6.0",
+	"hr": "6.0",
+	"flag_ht": "6.0",
+	"ht": "6.0",
+	"flag_hu": "6.0",
+	"hu": "6.0",
+	"flag_ic": "6.0",
+	"ic": "6.0",
+	"flag_id": "6.0",
+	"indonesia": "6.0",
+	"flag_ie": "6.0",
+	"ie": "6.0",
+	"flag_il": "6.0",
+	"il": "6.0",
+	"flag_im": "6.0",
+	"im": "6.0",
+	"flag_in": "6.0",
+	"in": "6.0",
+	"flag_io": "6.0",
+	"io": "6.0",
+	"flag_iq": "6.0",
+	"iq": "6.0",
+	"flag_ir": "6.0",
+	"ir": "6.0",
+	"flag_is": "6.0",
+	"is": "6.0",
+	"flag_it": "6.0",
+	"it": "6.0",
+	"flag_je": "6.0",
+	"je": "6.0",
+	"flag_jm": "6.0",
+	"jm": "6.0",
+	"flag_jo": "6.0",
+	"jo": "6.0",
+	"flag_jp": "6.0",
+	"jp": "6.0",
+	"flag_ke": "6.0",
+	"ke": "6.0",
+	"flag_kg": "6.0",
+	"kg": "6.0",
+	"flag_kh": "6.0",
+	"kh": "6.0",
+	"flag_ki": "6.0",
+	"ki": "6.0",
+	"flag_km": "6.0",
+	"km": "6.0",
+	"flag_kn": "6.0",
+	"kn": "6.0",
+	"flag_kp": "6.0",
+	"kp": "6.0",
+	"flag_kr": "6.0",
+	"kr": "6.0",
+	"flag_kw": "6.0",
+	"kw": "6.0",
+	"flag_ky": "6.0",
+	"ky": "6.0",
+	"flag_kz": "6.0",
+	"kz": "6.0",
+	"flag_la": "6.0",
+	"la": "6.0",
+	"flag_lb": "6.0",
+	"lb": "6.0",
+	"flag_lc": "6.0",
+	"lc": "6.0",
+	"flag_li": "6.0",
+	"li": "6.0",
+	"flag_lk": "6.0",
+	"lk": "6.0",
+	"flag_lr": "6.0",
+	"lr": "6.0",
+	"flag_ls": "6.0",
+	"ls": "6.0",
+	"flag_lt": "6.0",
+	"lt": "6.0",
+	"flag_lu": "6.0",
+	"lu": "6.0",
+	"flag_lv": "6.0",
+	"lv": "6.0",
+	"flag_ly": "6.0",
+	"ly": "6.0",
+	"flag_ma": "6.0",
+	"ma": "6.0",
+	"flag_mc": "6.0",
+	"mc": "6.0",
+	"flag_md": "6.0",
+	"md": "6.0",
+	"flag_me": "6.0",
+	"me": "6.0",
+	"flag_mf": "6.0",
+	"mf": "6.0",
+	"flag_mg": "6.0",
+	"mg": "6.0",
+	"flag_mh": "6.0",
+	"mh": "6.0",
+	"flag_mk": "6.0",
+	"mk": "6.0",
+	"flag_ml": "6.0",
+	"ml": "6.0",
+	"flag_mm": "6.0",
+	"mm": "6.0",
+	"flag_mn": "6.0",
+	"mn": "6.0",
+	"flag_mo": "6.0",
+	"mo": "6.0",
+	"flag_mp": "6.0",
+	"mp": "6.0",
+	"flag_mq": "6.0",
+	"mq": "6.0",
+	"flag_mr": "6.0",
+	"mr": "6.0",
+	"flag_ms": "6.0",
+	"ms": "6.0",
+	"flag_mt": "6.0",
+	"mt": "6.0",
+	"flag_mu": "6.0",
+	"mu": "6.0",
+	"flag_mv": "6.0",
+	"mv": "6.0",
+	"flag_mw": "6.0",
+	"mw": "6.0",
+	"flag_mx": "6.0",
+	"mx": "6.0",
+	"flag_my": "6.0",
+	"my": "6.0",
+	"flag_mz": "6.0",
+	"mz": "6.0",
+	"flag_na": "6.0",
+	"na": "6.0",
+	"flag_nc": "6.0",
+	"nc": "6.0",
+	"flag_ne": "6.0",
+	"ne": "6.0",
+	"flag_nf": "6.0",
+	"nf": "6.0",
+	"flag_ng": "6.0",
+	"nigeria": "6.0",
+	"flag_ni": "6.0",
+	"ni": "6.0",
+	"flag_nl": "6.0",
+	"nl": "6.0",
+	"flag_no": "6.0",
+	"no": "6.0",
+	"flag_np": "6.0",
+	"np": "6.0",
+	"flag_nr": "6.0",
+	"nr": "6.0",
+	"flag_nu": "6.0",
+	"nu": "6.0",
+	"flag_nz": "6.0",
+	"nz": "6.0",
+	"flag_om": "6.0",
+	"om": "6.0",
+	"flag_pa": "6.0",
+	"pa": "6.0",
+	"flag_pe": "6.0",
+	"pe": "6.0",
+	"flag_pf": "6.0",
+	"pf": "6.0",
+	"flag_pg": "6.0",
+	"pg": "6.0",
+	"flag_ph": "6.0",
+	"ph": "6.0",
+	"flag_pk": "6.0",
+	"pk": "6.0",
+	"flag_pl": "6.0",
+	"pl": "6.0",
+	"flag_pm": "6.0",
+	"pm": "6.0",
+	"flag_pn": "6.0",
+	"pn": "6.0",
+	"flag_pr": "6.0",
+	"pr": "6.0",
+	"flag_ps": "6.0",
+	"ps": "6.0",
+	"flag_pt": "6.0",
+	"pt": "6.0",
+	"flag_pw": "6.0",
+	"pw": "6.0",
+	"flag_py": "6.0",
+	"py": "6.0",
+	"flag_qa": "6.0",
+	"qa": "6.0",
+	"flag_re": "6.0",
+	"re": "6.0",
+	"flag_ro": "6.0",
+	"ro": "6.0",
+	"flag_rs": "6.0",
+	"rs": "6.0",
+	"flag_ru": "6.0",
+	"ru": "6.0",
+	"flag_rw": "6.0",
+	"rw": "6.0",
+	"flag_sa": "6.0",
+	"saudiarabia": "6.0",
+	"saudi": "6.0",
+	"flag_sb": "6.0",
+	"sb": "6.0",
+	"flag_sc": "6.0",
+	"sc": "6.0",
+	"flag_sd": "6.0",
+	"sd": "6.0",
+	"flag_se": "6.0",
+	"se": "6.0",
+	"flag_sg": "6.0",
+	"sg": "6.0",
+	"flag_sh": "6.0",
+	"sh": "6.0",
+	"flag_si": "6.0",
+	"si": "6.0",
+	"flag_sj": "6.0",
+	"sj": "6.0",
+	"flag_sk": "6.0",
+	"sk": "6.0",
+	"flag_sl": "6.0",
+	"sl": "6.0",
+	"flag_sm": "6.0",
+	"sm": "6.0",
+	"flag_sn": "6.0",
+	"sn": "6.0",
+	"flag_so": "6.0",
+	"so": "6.0",
+	"flag_sr": "6.0",
+	"sr": "6.0",
+	"flag_ss": "6.0",
+	"ss": "6.0",
+	"flag_st": "6.0",
+	"st": "6.0",
+	"flag_sv": "6.0",
+	"sv": "6.0",
+	"flag_sx": "6.0",
+	"sx": "6.0",
+	"flag_sy": "6.0",
+	"sy": "6.0",
+	"flag_sz": "6.0",
+	"sz": "6.0",
+	"flag_ta": "6.0",
+	"ta": "6.0",
+	"flag_tc": "6.0",
+	"tc": "6.0",
+	"flag_td": "6.0",
+	"td": "6.0",
+	"flag_tf": "6.0",
+	"tf": "6.0",
+	"flag_tg": "6.0",
+	"tg": "6.0",
+	"flag_th": "6.0",
+	"th": "6.0",
+	"flag_tj": "6.0",
+	"tj": "6.0",
+	"flag_tk": "6.0",
+	"tk": "6.0",
+	"flag_tl": "6.0",
+	"tl": "6.0",
+	"flag_tm": "6.0",
+	"turkmenistan": "6.0",
+	"flag_tn": "6.0",
+	"tn": "6.0",
+	"flag_to": "6.0",
+	"to": "6.0",
+	"flag_tr": "6.0",
+	"tr": "6.0",
+	"flag_tt": "6.0",
+	"tt": "6.0",
+	"flag_tv": "6.0",
+	"tuvalu": "6.0",
+	"flag_tw": "6.0",
+	"tw": "6.0",
+	"flag_tz": "6.0",
+	"tz": "6.0",
+	"flag_ua": "6.0",
+	"ua": "6.0",
+	"flag_ug": "6.0",
+	"ug": "6.0",
+	"flag_um": "6.0",
+	"um": "6.0",
+	"flag_us": "6.0",
+	"us": "6.0",
+	"flag_uy": "6.0",
+	"uy": "6.0",
+	"flag_uz": "6.0",
+	"uz": "6.0",
+	"flag_va": "6.0",
+	"va": "6.0",
+	"flag_vc": "6.0",
+	"vc": "6.0",
+	"flag_ve": "6.0",
+	"ve": "6.0",
+	"flag_vg": "6.0",
+	"vg": "6.0",
+	"flag_vi": "6.0",
+	"vi": "6.0",
+	"flag_vn": "6.0",
+	"vn": "6.0",
+	"flag_vu": "6.0",
+	"vu": "6.0",
+	"flag_wf": "6.0",
+	"wf": "6.0",
+	"flag_ws": "6.0",
+	"ws": "6.0",
+	"flag_xk": "6.0",
+	"xk": "6.0",
+	"flag_ye": "6.0",
+	"ye": "6.0",
+	"flag_yt": "6.0",
+	"yt": "6.0",
+	"flag_za": "6.0",
+	"za": "6.0",
+	"flag_zm": "6.0",
+	"zm": "6.0",
+	"flag_zw": "6.0",
+	"zw": "6.0",
+	"regional_indicator_z": "6.0",
+	"regional_indicator_y": "6.0",
+	"regional_indicator_x": "6.0",
+	"regional_indicator_w": "6.0",
+	"regional_indicator_v": "6.0",
+	"regional_indicator_u": "6.0",
+	"regional_indicator_t": "6.0",
+	"regional_indicator_s": "6.0",
+	"regional_indicator_r": "6.0",
+	"regional_indicator_q": "6.0",
+	"regional_indicator_p": "6.0",
+	"regional_indicator_o": "6.0",
+	"regional_indicator_n": "6.0",
+	"regional_indicator_m": "6.0",
+	"regional_indicator_l": "6.0",
+	"regional_indicator_k": "6.0",
+	"regional_indicator_j": "6.0",
+	"regional_indicator_i": "6.0",
+	"regional_indicator_h": "6.0",
+	"regional_indicator_g": "6.0",
+	"regional_indicator_f": "6.0",
+	"regional_indicator_e": "6.0",
+	"regional_indicator_d": "6.0",
+	"regional_indicator_c": "6.0",
+	"regional_indicator_b": "6.0",
+	"regional_indicator_a": "6.0",
+	"large_blue_circle": "6.0",
+	"ten": "6.0"
+}
\ No newline at end of file
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 789f45489eba5b4ac0e592cac7775706303da9e4..a5c9f0b509c39287540ccde133ac4d9453118df9 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -10,7 +10,7 @@ module API
       params do
         requires :id, type: String, desc: "The #{source_type} ID"
       end
-      resource source_type.pluralize do
+      resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
         desc "Gets a list of access requests for a #{source_type}." do
           detail 'This feature was introduced in GitLab 8.11.'
           success Entities::AccessRequester
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ed775f898d23b3d3c53c7babc1b6f10f89f7b018..7c7bfada7d0d86f38bee5b1ff6daf7812c6ced88 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -5,26 +5,42 @@ module API
     version %w(v3 v4), using: :path
 
     version 'v3', using: :path do
+      helpers ::API::V3::Helpers
+
+      mount ::API::V3::AwardEmoji
       mount ::API::V3::Boards
       mount ::API::V3::Branches
+      mount ::API::V3::BroadcastMessages
+      mount ::API::V3::Builds
       mount ::API::V3::Commits
       mount ::API::V3::DeployKeys
+      mount ::API::V3::Environments
       mount ::API::V3::Files
+      mount ::API::V3::Groups
       mount ::API::V3::Issues
       mount ::API::V3::Labels
       mount ::API::V3::Members
       mount ::API::V3::MergeRequestDiffs
       mount ::API::V3::MergeRequests
+      mount ::API::V3::Notes
+      mount ::API::V3::Pipelines
       mount ::API::V3::ProjectHooks
+      mount ::API::V3::Milestones
       mount ::API::V3::Projects
       mount ::API::V3::ProjectSnippets
       mount ::API::V3::Repositories
+      mount ::API::V3::Runners
+      mount ::API::V3::Services
+      mount ::API::V3::Settings
+      mount ::API::V3::Snippets
       mount ::API::V3::Subscriptions
       mount ::API::V3::SystemHooks
       mount ::API::V3::Tags
-      mount ::API::V3::Todos
       mount ::API::V3::Templates
+      mount ::API::V3::Todos
+      mount ::API::V3::Triggers
       mount ::API::V3::Users
+      mount ::API::V3::Variables
     end
 
     before { allow_access_with_scope :api }
@@ -47,6 +63,10 @@ module API
       error! e.message, e.status, e.headers
     end
 
+    rescue_from Gitlab::Auth::TooManyIps do |e|
+      rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+    end
+
     rescue_from :all do |exception|
       handle_api_exception(exception)
     end
@@ -64,7 +84,6 @@ module API
     mount ::API::Boards
     mount ::API::Branches
     mount ::API::BroadcastMessages
-    mount ::API::Builds
     mount ::API::Commits
     mount ::API::CommitStatuses
     mount ::API::DeployKeys
@@ -74,6 +93,7 @@ module API
     mount ::API::Groups
     mount ::API::Internal
     mount ::API::Issues
+    mount ::API::Jobs
     mount ::API::Keys
     mount ::API::Labels
     mount ::API::Lint
@@ -90,6 +110,7 @@ module API
     mount ::API::Projects
     mount ::API::ProjectSnippets
     mount ::API::Repositories
+    mount ::API::Runner
     mount ::API::Runners
     mount ::API::Services
     mount ::API::Session
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index df6db140d0e724e6292bbcce49a745054da8ea54..409cb5b924f249e1743f2cebca57f90d2f2fc6a9 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,7 +6,7 @@ module API
   module APIGuard
     extend ActiveSupport::Concern
 
-    PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+    PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
     PRIVATE_TOKEN_PARAM = :private_token
 
     included do |base|
@@ -114,8 +114,8 @@ module API
       private
 
       def install_error_responders(base)
-        error_classes = [ MissingTokenError, TokenNotFoundError,
-                          ExpiredError, RevokedError, InsufficientScopeError]
+        error_classes = [MissingTokenError, TokenNotFoundError,
+                         ExpiredError, RevokedError, InsufficientScopeError]
 
         base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
       end
@@ -160,13 +160,10 @@ module API
     # Exceptions
     #
 
-    class MissingTokenError < StandardError; end
-
-    class TokenNotFoundError < StandardError; end
-
-    class ExpiredError < StandardError; end
-
-    class RevokedError < StandardError; end
+    MissingTokenError = Class.new(StandardError)
+    TokenNotFoundError = Class.new(StandardError)
+    ExpiredError = Class.new(StandardError)
+    RevokedError = Class.new(StandardError)
 
     class InsufficientScopeError < StandardError
       attr_reader :scopes
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2ef327217ea7929796bc6e395aa0c29dfe859a6d..56f19f89642f8b264f07d2097981a59ada37399d 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -3,19 +3,26 @@ module API
     include PaginationParams
 
     before { authenticate! }
-    AWARDABLES = %w[issue merge_request snippet]
-
-    resource :projects do
-      AWARDABLES.each do |awardable_type|
-        awardable_string = awardable_type.pluralize
-        awardable_id_string = "#{awardable_type}_id"
+    AWARDABLES = [
+      { type: 'issue', find_by: :iid },
+      { type: 'merge_request', find_by: :iid },
+      { type: 'snippet', find_by: :id }
+    ].freeze
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
+      AWARDABLES.each do |awardable_params|
+        awardable_string = awardable_params[:type].pluralize
+        awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
 
         params do
-          requires :id, type: String, desc: 'The ID of a project'
           requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
         end
 
-        [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+        [
+          ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
           ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
         ].each do |endpoint|
 
@@ -82,7 +89,6 @@ module API
             unauthorized! unless award.user == current_user || current_user.admin?
 
             award.destroy
-            present award, with: Entities::AwardEmoji
           end
         end
       end
@@ -104,10 +110,10 @@ module API
               note_id = params.delete(:note_id)
 
               awardable.notes.find(note_id)
-            elsif params.include?(:issue_id)
-              user_project.issues.find(params[:issue_id])
-            elsif params.include?(:merge_request_id)
-              user_project.merge_requests.find(params[:merge_request_id])
+            elsif params.include?(:issue_iid)
+              user_project.issues.find_by!(iid: params[:issue_iid])
+            elsif params.include?(:merge_request_iid)
+              user_project.merge_requests.find_by!(iid: params[:merge_request_iid])
             else
               user_project.snippets.find(params[:snippet_id])
             end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index f4226e5a89d759235177a299a195957072d73df0..5a2d7a681e3a5cec68d1ec8a27c83cb903508c22 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -7,7 +7,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get all project boards' do
         detail 'This feature was introduced in 8.13'
         success Entities::Board
@@ -127,9 +127,7 @@ module API
 
           service = ::Boards::Lists::DestroyService.new(user_project, current_user)
 
-          if service.execute(list)
-            present list, with: Entities::List
-          else
+          unless service.execute(list)
             render_api_error!({ error: 'List could not be deleted!' }, 400)
           end
         end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index c65de90cca29c23064d40faf362094cddf04fd92..f35084a582ac286f5f69f8f2b1c42c996d975bf5 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -4,13 +4,12 @@ module API
   class Branches < Grape::API
     include PaginationParams
 
-    before { authenticate! }
     before { authorize! :download_code, user_project }
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a project repository branches' do
         success Entities::RepoBranch
       end
@@ -102,6 +101,7 @@ module API
       end
       post ":id/repository/branches" do
         authorize_push_project
+
         result = CreateBranchService.new(user_project, current_user).
                  execute(params[:branch], params[:ref])
 
@@ -124,11 +124,7 @@ module API
         result = DeleteBranchService.new(user_project, current_user).
                  execute(params[:branch])
 
-        if result[:status] == :success
-          {
-            branch: params[:branch]
-          }
-        else
+        if result[:status] != :success
           render_api_error!(result[:message], result[:return_code])
         end
       end
@@ -137,7 +133,7 @@ module API
       delete ":id/repository/merged_branches" do
         DeleteMergedBranchesService.new(user_project, current_user).async_execute
 
-        status(200)
+        accepted!
       end
     end
   end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 1217002bf8ec526983e358191df3c9b406cc32ea..395c401203c4aa3e216dcc24b30ace9ef68cf682 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -91,7 +91,7 @@ module API
       delete ':id' do
         message = find_message
 
-        present message.destroy, with: Entities::BroadcastMessage
+        message.destroy
       end
     end
   end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 0b6076bd28c3a786de7e4c860e3bd5feead27974..827a38d33da7ac25a4cf70b853805d0f9617cd01 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -2,7 +2,10 @@ require 'mime/types'
 
 module API
   class CommitStatuses < Grape::API
-    resource :projects do
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
       include PaginationParams
 
       before { authenticate! }
@@ -11,7 +14,6 @@ module API
         success Entities::CommitStatus
       end
       params do
-        requires :id,    type: String, desc: 'The ID of a project'
         requires :sha,   type: String, desc: 'The commit hash'
         optional :ref,   type: String, desc: 'The ref'
         optional :stage, type: String, desc: 'The stage'
@@ -37,10 +39,9 @@ module API
         success Entities::CommitStatus
       end
       params do
-        requires :id,          type: String,  desc: 'The ID of a project'
         requires :sha,         type: String,  desc: 'The commit hash'
         requires :state,       type: String,  desc: 'The state of the status',
-                               values: ['pending', 'running', 'success', 'failed', 'canceled']
+                               values: %w(pending running success failed canceled)
         optional :ref,         type: String,  desc: 'The ref'
         optional :target_url,  type: String,  desc: 'The target URL to associate with this status'
         optional :description, type: String,  desc: 'A short description of the status'
@@ -72,14 +73,15 @@ module API
         status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
           project: @project,
           pipeline: pipeline,
-          user: current_user,
           name: name,
           ref: ref,
-          target_url: params[:target_url],
-          description: params[:description],
-          coverage: params[:coverage]
+          user: current_user
         )
 
+        optional_attributes =
+          attributes_for_keys(%w[target_url description coverage])
+
+        status.update(optional_attributes) if optional_attributes.any?
         render_validation_error!(status) if status.invalid?
 
         begin
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 0cd817f935262edd31fcd4771c1af3041dd335e8..66b37fd2bcc4f6614d865360f0bad9f4e6c61df5 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -10,7 +10,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a project repository commits' do
         success Entities::RepoCommit
       end
@@ -18,22 +18,34 @@ module API
         optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
         optional :since,    type: DateTime, desc: 'Only commits after or on this date will be returned'
         optional :until,    type: DateTime, desc: 'Only commits before or on this date will be returned'
-        optional :page,     type: Integer, default: 0, desc: 'The page for pagination'
-        optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
         optional :path,     type: String, desc: 'The file path'
+        use :pagination
       end
       get ":id/repository/commits" do
-        ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
-        offset = params[:page] * params[:per_page]
+        path   = params[:path]
+        before = params[:until]
+        after  = params[:since]
+        ref    = params[:ref_name] || user_project.try(:default_branch) || 'master'
+        offset = (params[:page] - 1) * params[:per_page]
 
         commits = user_project.repository.commits(ref,
-                                                  path: params[:path],
+                                                  path: path,
                                                   limit: params[:per_page],
                                                   offset: offset,
-                                                  after: params[:since],
-                                                  before: params[:until])
+                                                  before: before,
+                                                  after: after)
+
+        commit_count =
+          if path || before || after
+            user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+          else
+            # Cacheable commit count.
+            user_project.repository.commit_count_for_ref(ref)
+          end
+
+        paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
 
-        present commits, with: Entities::RepoCommit
+        present paginate(paginated_commits), with: Entities::RepoCommit
       end
 
       desc 'Commit multiple file changes as one commit' do
@@ -52,13 +64,6 @@ module API
 
         attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
 
-        attrs[:actions].map! do |action|
-          action[:action] = action[:action].to_sym
-          action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
-          action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
-          action
-        end
-
         result = ::Files::MultiService.new(user_project, current_user, attrs).execute
 
         if result[:status] == :success
@@ -134,7 +139,7 @@ module API
 
         commit_params = {
           commit: commit,
-          create_merge_request: false,
+          start_branch: params[:branch],
           target_branch: params[:branch]
         }
 
@@ -157,7 +162,7 @@ module API
         optional :path, type: String, desc: 'The file path'
         given :path do
           requires :line, type: Integer, desc: 'The line number'
-          requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+          requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
         end
       end
       post ':id/repository/commits/:sha/comments' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 69e85c27a65d6063daa595f753084472b9aa731d..b888ede6fe8311bca656eb25ff0497ae505f8a6a 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -17,7 +17,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of the project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       before { authorize_admin_project }
 
       desc "Get a specific project's deploy keys" do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index c5feb49b22fae593e890ec157d4f8b35f0b6aab8..46b936897f6aaa5cf9357633f89760d2b6d01f87 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -1,5 +1,5 @@
 module API
-  # Deployments RESTfull API endpoints
+  # Deployments RESTful API endpoints
   class Deployments < Grape::API
     include PaginationParams
 
@@ -8,7 +8,7 @@ module API
     params do
       requires :id, type: String, desc: 'The project ID'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get all deployments of the project' do
         detail 'This feature was introduced in GitLab 8.11.'
         success Entities::Deployment
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 400ee7c92aa7e159de2b28103c4f65826225bc81..5954aea80411d40c6b49d6b8dc5b2e1cfc3ee697 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -49,7 +49,8 @@ module API
 
     class ProjectHook < Hook
       expose :project_id, :issues_events, :merge_requests_events
-      expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+      expose :note_events, :pipeline_events, :wiki_page_events
+      expose :build_events, as: :job_events
     end
 
     class BasicProjectDetails < Grape::Entity
@@ -69,9 +70,8 @@ module API
 
     class Project < Grape::Entity
       expose :id, :description, :default_branch, :tag_list
-      expose :public?, as: :public
       expose :archived?, as: :archived
-      expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+      expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
       expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
       expose :name, :name_with_namespace
       expose :path, :path_with_namespace
@@ -81,7 +81,7 @@ module API
       expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
       expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
       expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
-      expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+      expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
       expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
 
       expose :created_at, :last_activity_at
@@ -94,11 +94,11 @@ module API
       expose :star_count, :forks_count
       expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
       expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
-      expose :public_builds
+      expose :public_builds, as: :public_jobs
       expose :shared_with_groups do |project, options|
         SharedGroup.represent(project.project_group_links.all, options)
       end
-      expose :only_allow_merge_if_build_succeeds
+      expose :only_allow_merge_if_pipeline_succeeds
       expose :request_access_enabled
       expose :only_allow_merge_if_all_discussions_are_resolved
 
@@ -110,7 +110,7 @@ module API
       expose :storage_size
       expose :repository_size
       expose :lfs_objects_size
-      expose :build_artifacts_size
+      expose :build_artifacts_size, as: :job_artifacts_size
     end
 
     class Member < UserBasic
@@ -132,7 +132,7 @@ module API
     end
 
     class Group < Grape::Entity
-      expose :id, :name, :path, :description, :visibility_level
+      expose :id, :name, :path, :description, :visibility
       expose :lfs_enabled?, as: :lfs_enabled
       expose :avatar_url
       expose :web_url
@@ -145,7 +145,7 @@ module API
           expose :storage_size
           expose :repository_size
           expose :lfs_objects_size
-          expose :build_artifacts_size
+          expose :build_artifacts_size, as: :job_artifacts_size
         end
       end
     end
@@ -250,14 +250,11 @@ module API
       expose :start_date
     end
 
-    class Issue < ProjectEntity
+    class IssueBasic < ProjectEntity
       expose :label_names, as: :labels
       expose :milestone, using: Entities::Milestone
       expose :assignee, :author, using: Entities::UserBasic
 
-      expose :subscribed do |issue, options|
-        issue.subscribed?(options[:current_user], options[:project] || issue.project)
-      end
       expose :user_notes_count
       expose :upvotes, :downvotes
       expose :due_date
@@ -268,6 +265,12 @@ module API
       end
     end
 
+    class Issue < IssueBasic
+      expose :subscribed do |issue, options|
+        issue.subscribed?(options[:current_user], options[:project] || issue.project)
+      end
+    end
+
     class IssuableTimeStats < Grape::Entity
       expose :time_estimate
       expose :total_time_spent
@@ -280,7 +283,7 @@ module API
       expose :id
     end
 
-    class MergeRequest < ProjectEntity
+    class MergeRequestBasic < ProjectEntity
       expose :target_branch, :source_branch
       expose :upvotes, :downvotes
       expose :author, :assignee, using: Entities::UserBasic
@@ -288,13 +291,10 @@ module API
       expose :label_names, as: :labels
       expose :work_in_progress?, as: :work_in_progress
       expose :milestone, using: Entities::Milestone
-      expose :merge_when_build_succeeds
+      expose :merge_when_pipeline_succeeds
       expose :merge_status
       expose :diff_head_sha, as: :sha
       expose :merge_commit_sha
-      expose :subscribed do |merge_request, options|
-        merge_request.subscribed?(options[:current_user], options[:project])
-      end
       expose :user_notes_count
       expose :should_remove_source_branch?, as: :should_remove_source_branch
       expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -304,6 +304,12 @@ module API
       end
     end
 
+    class MergeRequest < MergeRequestBasic
+      expose :subscribed do |merge_request, options|
+        merge_request.subscribed?(options[:current_user], options[:project])
+      end
+    end
+
     class MergeRequestChanges < MergeRequest
       expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
         compare.raw_diffs(all_diffs: true).to_a
@@ -339,9 +345,6 @@ module API
       expose :created_at, :updated_at
       expose :system?, as: :system
       expose :noteable_id, :noteable_type
-      # upvote? and downvote? are deprecated, always return false
-      expose(:upvote?)    { |note| false }
-      expose(:downvote?)  { |note| false }
     end
 
     class AwardEmoji < Grape::Entity
@@ -397,7 +400,8 @@ module API
       expose :target_type
 
       expose :target do |todo, options|
-        Entities.const_get(todo.target_type).represent(todo.target, options)
+        target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
+        Entities.const_get(target).represent(todo.target, options)
       end
 
       expose :target_url do |todo, options|
@@ -451,7 +455,8 @@ module API
     class ProjectService < Grape::Entity
       expose :id, :title, :created_at, :updated_at, :active
       expose :push_events, :issues_events, :merge_requests_events
-      expose :tag_push_events, :note_events, :build_events, :pipeline_events
+      expose :tag_push_events, :note_events, :pipeline_events
+      expose :build_events, as: :job_events
       # Expose serialized properties
       expose :properties do |service, options|
         field_names = service.fields.
@@ -554,12 +559,15 @@ module API
       expose :updated_at
       expose :home_page_url
       expose :default_branch_protection
-      expose :restricted_visibility_levels
+      expose(:restricted_visibility_levels) do |setting, _options|
+        setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
+      end
       expose :max_attachment_size
       expose :session_expire_delay
-      expose :default_project_visibility
-      expose :default_snippet_visibility
-      expose :default_group_visibility
+      expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
+      expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
+      expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
+      expose :default_artifacts_expire_in
       expose :domain_whitelist
       expose :domain_blacklist_enabled
       expose :domain_blacklist
@@ -592,10 +600,6 @@ module API
       end
     end
 
-    class TriggerRequest < Grape::Entity
-      expose :id, :variables
-    end
-
     class Runner < Grape::Entity
       expose :id
       expose :description
@@ -620,7 +624,11 @@ module API
       end
     end
 
-    class BuildArtifactFile < Grape::Entity
+    class RunnerRegistrationDetails < Grape::Entity
+      expose :id, :token
+    end
+
+    class JobArtifactFile < Grape::Entity
       expose :filename, :size
     end
 
@@ -628,18 +636,21 @@ module API
       expose :id, :sha, :ref, :status
     end
 
-    class Build < Grape::Entity
+    class Job < Grape::Entity
       expose :id, :status, :stage, :name, :ref, :tag, :coverage
       expose :created_at, :started_at, :finished_at
       expose :user, with: User
-      expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+      expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
       expose :commit, with: RepoCommit
       expose :runner, with: Runner
       expose :pipeline, with: PipelineBasic
     end
 
     class Trigger < Grape::Entity
-      expose :token, :created_at, :updated_at, :deleted_at, :last_used
+      expose :id
+      expose :token, :description
+      expose :created_at, :updated_at, :deleted_at, :last_used
+      expose :owner, using: Entities::UserBasic
     end
 
     class Variable < Grape::Entity
@@ -660,14 +671,14 @@ module API
     end
 
     class Environment < EnvironmentBasic
-      expose :project, using: Entities::Project
+      expose :project, using: Entities::BasicProjectDetails
     end
 
     class Deployment < Grape::Entity
       expose :id, :iid, :ref, :sha, :created_at
       expose :user,        using: Entities::UserBasic
       expose :environment, using: Entities::EnvironmentBasic
-      expose :deployable,  using: Entities::Build
+      expose :deployable,  using: Entities::Job
     end
 
     class RepoLicense < Grape::Entity
@@ -694,5 +705,99 @@ module API
       expose :id, :message, :starts_at, :ends_at, :color, :font
       expose :active?, as: :active
     end
+
+    class PersonalAccessToken < Grape::Entity
+      expose :id, :name, :revoked, :created_at, :scopes
+      expose :active?, as: :active
+      expose :expires_at do |personal_access_token|
+        personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
+      end
+    end
+
+    class PersonalAccessTokenWithToken < PersonalAccessToken
+      expose :token
+    end
+
+    class ImpersonationToken < PersonalAccessTokenWithToken
+      expose :impersonation
+    end
+
+    module JobRequest
+      class JobInfo < Grape::Entity
+        expose :name, :stage
+        expose :project_id, :project_name
+      end
+
+      class GitInfo < Grape::Entity
+        expose :repo_url, :ref, :sha, :before_sha
+        expose :ref_type do |model|
+          if model.tag
+            'tag'
+          else
+            'branch'
+          end
+        end
+      end
+
+      class RunnerInfo < Grape::Entity
+        expose :timeout
+      end
+
+      class Step < Grape::Entity
+        expose :name, :script, :timeout, :when, :allow_failure
+      end
+
+      class Image < Grape::Entity
+        expose :name
+      end
+
+      class Artifacts < Grape::Entity
+        expose :name, :untracked, :paths, :when, :expire_in
+      end
+
+      class Cache < Grape::Entity
+        expose :key, :untracked, :paths
+      end
+
+      class Credentials < Grape::Entity
+        expose :type, :url, :username, :password
+      end
+
+      class ArtifactFile < Grape::Entity
+        expose :filename, :size
+      end
+
+      class Dependency < Grape::Entity
+        expose :id, :name, :token
+        expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
+      end
+
+      class Response < Grape::Entity
+        expose :id
+        expose :token
+        expose :allow_git_fetch
+
+        expose :job_info, using: JobInfo do |model|
+          model
+        end
+
+        expose :git_info, using: GitInfo do |model|
+          model
+        end
+
+        expose :runner_info, using: RunnerInfo do |model|
+          model
+        end
+
+        expose :variables
+        expose :steps, using: Step
+        expose :image, using: Image
+        expose :services, using: Image
+        expose :artifacts, using: Artifacts
+        expose :cache, using: Cache
+        expose :credentials, using: Credentials
+        expose :dependencies, using: Dependency
+      end
+    end
   end
 end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 1a7e68f0528f3d15f628d48fb3a47c824ce3c2e5..945771d46f3b72758862cb2f47f460b5f3cf3d04 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -9,7 +9,7 @@ module API
     params do
       requires :id, type: String, desc: 'The project ID'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get all environments of the project' do
         detail 'This feature was introduced in GitLab 8.11.'
         success Entities::Environment
@@ -79,7 +79,24 @@ module API
 
         environment = user_project.environments.find(params[:environment_id])
 
-        present environment.destroy, with: Entities::Environment
+        environment.destroy
+      end
+
+      desc 'Stops an existing environment' do
+        success Entities::Environment
+      end
+      params do
+        requires :environment_id, type: Integer,  desc: 'The environment ID'
+      end
+      post ':id/environments/:environment_id/stop' do
+        authorize! :create_deployment, user_project
+
+        environment = user_project.environments.find(params[:environment_id])
+
+        environment.stop_with_action!(current_user)
+
+        status 200
+        present environment, with: Entities::Environment
       end
     end
   end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 500f9d3c787cbb18d534b022113af99ddadea04b..33fc970dc09df12c922997f119cd45c0df5e92fc 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -14,6 +14,19 @@ module API
         }
       end
 
+      def assign_file_vars!
+        authorize! :download_code, user_project
+
+        @commit = user_project.commit(params[:ref])
+        not_found!('Commit') unless @commit
+
+        @repo = user_project.repository
+        @blob = @repo.blob_at(@commit.sha, params[:file_path])
+
+        not_found!('File') unless @blob
+        @blob.load_all_data!(@repo)
+      end
+
       def commit_response(attrs)
         {
           file_path: attrs[:file_path],
@@ -22,7 +35,7 @@ module API
       end
 
       params :simple_file_params do
-        requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+        requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
         requires :branch, type: String, desc: 'The name of branch'
         requires :commit_message, type: String, desc: 'Commit Message'
         optional :author_email, type: String, desc: 'The email of the author'
@@ -39,35 +52,36 @@ module API
     params do
       requires :id, type: String, desc: 'The project ID'
     end
-    resource :projects do
-      desc 'Get a file from repository'
+    resource :projects, requirements: { id: %r{[^/]+} } do
+      desc 'Get raw file contents from the repository'
       params do
-        requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
-        requires :ref, type: String, desc: 'The name of branch, tag, or commit'
+        requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+        requires :ref, type: String, desc: 'The name of branch, tag commit'
       end
-      get ":id/repository/files" do
-        authorize! :download_code, user_project
-
-        commit = user_project.commit(params[:ref])
-        not_found!('Commit') unless commit
+      get ":id/repository/files/:file_path/raw" do
+        assign_file_vars!
 
-        repo = user_project.repository
-        blob = repo.blob_at(commit.sha, params[:file_path])
-        not_found!('File') unless blob
+        send_git_blob @repo, @blob
+      end
 
-        blob.load_all_data!(repo)
-        status(200)
+      desc 'Get a file from the repository'
+      params do
+        requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+        requires :ref, type: String, desc: 'The name of branch, tag or commit'
+      end
+      get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+        assign_file_vars!
 
         {
-          file_name: blob.name,
-          file_path: blob.path,
-          size: blob.size,
+          file_name: @blob.name,
+          file_path: @blob.path,
+          size: @blob.size,
           encoding: "base64",
-          content: Base64.strict_encode64(blob.data),
+          content: Base64.strict_encode64(@blob.data),
           ref: params[:ref],
-          blob_id: blob.id,
-          commit_id: commit.id,
-          last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
+          blob_id: @blob.id,
+          commit_id: @commit.id,
+          last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
         }
       end
 
@@ -75,7 +89,7 @@ module API
       params do
         use :extended_file_params
       end
-      post ":id/repository/files" do
+      post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
         authorize! :push_code, user_project
 
         file_params = declared_params(include_missing: false)
@@ -93,7 +107,7 @@ module API
       params do
         use :extended_file_params
       end
-      put ":id/repository/files" do
+      put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
         authorize! :push_code, user_project
 
         file_params = declared_params(include_missing: false)
@@ -112,16 +126,13 @@ module API
       params do
         use :simple_file_params
       end
-      delete ":id/repository/files" do
+      delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
         authorize! :push_code, user_project
 
         file_params = declared_params(include_missing: false)
         result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
 
-        if result[:status] == :success
-          status(200)
-          commit_response(file_params)
-        else
+        if result[:status] != :success
           render_api_error!(result[:message], 400)
         end
       end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9f29c4466ab04e041cd195df9721abe19a1ab1fd..8f3799417e3bd03bdd5af3d5075962b8278cf0b3 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -7,7 +7,7 @@ module API
     helpers do
       params :optional_params do
         optional :description, type: String, desc: 'The description of the group'
-        optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+        optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
         optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
         optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
       end
@@ -36,12 +36,15 @@ module API
         optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
         optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
         optional :search, type: String, desc: 'Search for a specific group'
+        optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
         optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
         optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
         use :pagination
       end
       get do
-        groups = if current_user.admin
+        groups = if params[:owned]
+                   current_user.owned_groups
+                 elsif current_user.admin
                    Group.all
                  elsif params[:all_available]
                    GroupsFinder.new.execute(current_user)
@@ -56,17 +59,6 @@ module API
         present_groups groups, statistics: params[:statistics] && current_user.is_admin?
       end
 
-      desc 'Get list of owned groups for authenticated user' do
-        success Entities::Group
-      end
-      params do
-        use :pagination
-        use :statistics_params
-      end
-      get '/owned' do
-        present_groups current_user.owned_groups, statistics: params[:statistics]
-      end
-
       desc 'Create a group. Available only for users who can create groups.' do
         success Entities::Group
       end
@@ -92,7 +84,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a group'
     end
-    resource :groups do
+    resource :groups, requirements: { id: %r{[^/]+} } do
       desc 'Update a group. Available only for users who can administrate groups.' do
         success Entities::Group
       end
@@ -100,7 +92,7 @@ module API
         optional :name, type: String, desc: 'The name of the group'
         optional :path, type: String, desc: 'The path of the group'
         use :optional_params
-        at_least_one_of :name, :path, :description, :visibility_level,
+        at_least_one_of :name, :path, :description, :visibility,
                         :lfs_enabled, :request_access_enabled
       end
       put ':id' do
@@ -134,7 +126,7 @@ module API
       end
       params do
         optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
-        optional :visibility, type: String, values: %w[public internal private],
+        optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
                               desc: 'Limit by visibility'
         optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
         optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
@@ -162,7 +154,7 @@ module API
       params do
         requires :project_id, type: String, desc: 'The ID or path of the project'
       end
-      post ":id/projects/:project_id" do
+      post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
         authenticated_as_admin!
         group = find_group!(params[:id])
         project = find_project!(params[:project_id])
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 0fd2b1587e31f6356d7c95c21818d88d563eec76..3c173b544aa33d1158fd4a8ec1095b17e53ee5da 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,7 +3,7 @@ module API
     include Gitlab::Utils
     include Helpers::Pagination
 
-    SUDO_HEADER = "HTTP_SUDO"
+    SUDO_HEADER = "HTTP_SUDO".freeze
     SUDO_PARAM = :sudo
 
     def declared_params(options = {})
@@ -82,22 +82,22 @@ module API
       label || not_found!('Label')
     end
 
-    def find_project_issue(id)
-      IssuesFinder.new(current_user, project_id: user_project.id).find(id)
+    def find_project_issue(iid)
+      IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
     end
 
-    def find_project_merge_request(id)
-      MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+    def find_project_merge_request(iid)
+      MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
     end
 
-    def find_merge_request_with_access(id, access_level = :read_merge_request)
-      merge_request = user_project.merge_requests.find(id)
+    def find_merge_request_with_access(iid, access_level = :read_merge_request)
+      merge_request = user_project.merge_requests.find_by!(iid: iid)
       authorize! access_level, merge_request
       merge_request
     end
 
     def authenticate!
-      unauthorized! unless current_user
+      unauthorized! unless current_user && can?(current_user, :access_api)
     end
 
     def authenticate_non_get!
@@ -126,7 +126,7 @@ module API
       forbidden! unless current_user.is_admin?
     end
 
-    def authorize!(action, subject = nil)
+    def authorize!(action, subject = :global)
       forbidden! unless can?(current_user, action, subject)
     end
 
@@ -144,7 +144,7 @@ module API
       end
     end
 
-    def can?(object, action, subject)
+    def can?(object, action, subject = :global)
       Ability.allowed?(object, action, subject)
     end
 
@@ -174,6 +174,10 @@ module API
       items.where(iid: iid)
     end
 
+    def filter_by_search(items, text)
+      items.search(text)
+    end
+
     # error helpers
 
     def forbidden!(reason = nil)
@@ -219,6 +223,10 @@ module API
       render_api_error!('204 No Content', 204)
     end
 
+    def accepted!
+      render_api_error!('202 Accepted', 202)
+    end
+
     def render_validation_error!(model)
       if model.errors.any?
         render_api_error!(model.errors.messages || '400 Bad Request', 400)
@@ -254,6 +262,10 @@ module API
     # project helpers
 
     def filter_projects(projects)
+      if params[:membership]
+        projects = projects.merge(current_user.authorized_projects)
+      end
+
       if params[:owned]
         projects = projects.merge(current_user.owned_projects)
       end
@@ -334,16 +346,17 @@ module API
 
     def initial_current_user
       return @initial_current_user if defined?(@initial_current_user)
+      Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+        @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+        @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+        @initial_current_user ||= find_user_from_warden
 
-      @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?
+          @initial_current_user = nil
+        end
 
-      unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
-        @initial_current_user = nil
+        @initial_current_user
       end
-
-      @initial_current_user
     end
 
     def sudo!
@@ -386,14 +399,6 @@ module API
       header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
     end
 
-    def issue_entity(project)
-      if project.has_external_issue_tracker?
-        Entities::ExternalIssue
-      else
-        Entities::Issue
-      end
-    end
-
     # The Grape Error Middleware only has access to env but no params. We workaround this by
     # defining a method that returns the right value.
     def define_params_for_grape_middleware
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 080a627495700de2cba6a68d92433061f728e85d..2135a787b11a97136c11e76e9e155b3d023e75c2 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -9,11 +9,11 @@ module API
       # In addition, they may have a '.git' extension and multiple namespaces
       #
       # Transform all these cases to 'namespace/project'
-      def clean_project_path(project_path, storage_paths = Repository.storages.values)
+      def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values)
         project_path = project_path.sub(/\.git\z/, '')
 
-        storage_paths.each do |storage_path|
-          storage_path = File.expand_path(storage_path)
+        storages.each do |storage|
+          storage_path = File.expand_path(storage['path'])
 
           if project_path.start_with?(storage_path)
             project_path = project_path.sub(storage_path, '')
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74848a6e1442297d71399c07663317ee3ef42fc0
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -0,0 +1,69 @@
+module API
+  module Helpers
+    module Runner
+      JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+      JOB_TOKEN_PARAM = :token
+      UPDATE_RUNNER_EVERY = 10 * 60
+
+      def runner_registration_token_valid?
+        ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
+                                                                  current_application_settings.runners_registration_token)
+      end
+
+      def get_runner_version_from_params
+        return unless params['info'].present?
+        attributes_for_keys(%w(name version revision platform architecture), params['info'])
+      end
+
+      def authenticate_runner!
+        forbidden! unless current_runner
+      end
+
+      def current_runner
+        @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
+      end
+
+      def update_runner_info
+        return unless update_runner?
+
+        current_runner.contacted_at = Time.now
+        current_runner.assign_attributes(get_runner_version_from_params)
+        current_runner.save if current_runner.changed?
+      end
+
+      def update_runner?
+        # Use a random threshold to prevent beating DB updates.
+        # It generates a distribution between [40m, 80m].
+        #
+        contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
+
+        current_runner.contacted_at.nil? ||
+          (Time.now - current_runner.contacted_at) >= contacted_at_max_age
+      end
+
+      def validate_job!(job)
+        not_found! unless job
+
+        yield if block_given?
+
+        forbidden!('Project has been deleted!') unless job.project
+        forbidden!('Job has been erased!') if job.erased?
+      end
+
+      def authenticate_job!(job)
+        validate_job!(job) do
+          forbidden! unless job_token_valid?(job)
+        end
+      end
+
+      def job_token_valid?(job)
+        token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
+        token && job.valid_token?(token)
+      end
+
+      def max_artifacts_size
+        current_application_settings.max_artifacts_size.megabytes.to_i
+      end
+    end
+  end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d235977fbd843d136a06e2e4646583d28a35599e..7eed93aba0060140e14e8cc7c7526cf9180ee8d7 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -132,6 +132,18 @@ module API
 
         { success: true, recovery_codes: codes }
       end
+
+      post "/notify_post_receive" do
+        status 200
+
+        return unless Gitlab::GitalyClient.enabled?
+
+        begin
+          Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path])
+        rescue GRPC::Unavailable => e
+          render_api_error(e, 500)
+        end
+      end
     end
   end
 end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 6d30c5d81b12df8d183dbdb4d6a73c734b36e917..fd2674910d28923dc80b4764080a2ef5517bb54f 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -25,6 +25,7 @@ module API
         optional :sort, type: String, values: %w[asc desc], default: 'desc',
                         desc: 'Return issues sorted in `asc` or `desc` order.'
         optional :milestone, type: String, desc: 'Return issues for a specific milestone'
+        optional :iids, type: Array[Integer], desc: 'The IID array of issues'
         use :pagination
       end
 
@@ -40,7 +41,7 @@ module API
 
     resource :issues do
       desc "Get currently authenticated user's issues" do
-        success Entities::Issue
+        success Entities::IssueBasic
       end
       params do
         optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -50,16 +51,16 @@ module API
       get do
         issues = find_issues(scope: 'authored')
 
-        present paginate(issues), with: Entities::Issue, current_user: current_user
+        present paginate(issues), with: Entities::IssueBasic, current_user: current_user
       end
     end
 
     params do
       requires :id, type: String, desc: 'The ID of a group'
     end
-    resource :groups do
+    resource :groups, requirements: { id: %r{[^/]+} } do
       desc 'Get a list of group issues' do
-        success Entities::Issue
+        success Entities::IssueBasic
       end
       params do
         optional :state, type: String, values: %w[opened closed all], default: 'opened',
@@ -71,18 +72,18 @@ module API
 
         issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
 
-        present paginate(issues), with: Entities::Issue, current_user: current_user
+        present paginate(issues), with: Entities::IssueBasic, current_user: current_user
       end
     end
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       include TimeTrackingEndpoints
 
       desc 'Get a list of project issues' do
-        success Entities::Issue
+        success Entities::IssueBasic
       end
       params do
         optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -90,21 +91,21 @@ module API
         use :issues_params
       end
       get ":id/issues" do
-        project = find_project(params[:id])
+        project = find_project!(params[:id])
 
         issues = find_issues(project_id: project.id)
 
-        present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+        present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
       end
 
       desc 'Get a single project issue' do
         success Entities::Issue
       end
       params do
-        requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
       end
-      get ":id/issues/:issue_id" do
-        issue = find_project_issue(params[:issue_id])
+      get ":id/issues/:issue_iid" do
+        issue = find_project_issue(params[:issue_iid])
         present issue, with: Entities::Issue, current_user: current_user, project: user_project
       end
 
@@ -115,8 +116,10 @@ module API
         requires :title, type: String, desc: 'The title of an issue'
         optional :created_at, type: DateTime,
                               desc: 'Date time when the issue was created. Available only for admins and project owners.'
-        optional :merge_request_for_resolving_discussions, type: Integer,
+        optional :merge_request_to_resolve_discussions_of, type: Integer,
                                                            desc: 'The IID of a merge request for which to resolve discussions'
+        optional :discussion_to_resolve, type: String,
+                                         desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
         use :issue_params
       end
       post ':id/issues' do
@@ -127,12 +130,6 @@ module API
 
         issue_params = declared_params(include_missing: false)
 
-        if merge_request_iid = params[:merge_request_for_resolving_discussions]
-          issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
-            execute.
-            find_by(iid: merge_request_iid)
-        end
-
         issue = ::Issues::CreateService.new(user_project,
                                             current_user,
                                             issue_params.merge(request: request, api: true)).execute
@@ -151,7 +148,7 @@ module API
         success Entities::Issue
       end
       params do
-        requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
         optional :title, type: String, desc: 'The title of an issue'
         optional :updated_at, type: DateTime,
                               desc: 'Date time when the issue was updated. Available only for admins and project owners.'
@@ -160,8 +157,8 @@ module API
         at_least_one_of :title, :description, :assignee_id, :milestone_id,
                         :labels, :created_at, :due_date, :confidential, :state_event
       end
-      put ':id/issues/:issue_id' do
-        issue = user_project.issues.find(params.delete(:issue_id))
+      put ':id/issues/:issue_iid' do
+        issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
         authorize! :update_issue, issue
 
         # Setting created_at time only allowed for admins and project owners
@@ -188,11 +185,11 @@ module API
         success Entities::Issue
       end
       params do
-        requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
         requires :to_project_id, type: Integer, desc: 'The ID of the new project'
       end
-      post ':id/issues/:issue_id/move' do
-        issue = user_project.issues.find_by(id: params[:issue_id])
+      post ':id/issues/:issue_iid/move' do
+        issue = user_project.issues.find_by(iid: params[:issue_iid])
         not_found!('Issue') unless issue
 
         new_project = Project.find_by(id: params[:to_project_id])
@@ -208,10 +205,10 @@ module API
 
       desc 'Delete a project issue'
       params do
-        requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
       end
-      delete ":id/issues/:issue_id" do
-        issue = user_project.issues.find_by(id: params[:issue_id])
+      delete ":id/issues/:issue_iid" do
+        issue = user_project.issues.find_by(iid: params[:issue_iid])
         not_found!('Issue') unless issue
 
         authorize!(:destroy_issue, issue)
diff --git a/lib/api/builds.rb b/lib/api/jobs.rb
similarity index 51%
rename from lib/api/builds.rb
rename to lib/api/jobs.rb
index 44fe0fc4a95aff85770673ae568d71d65e4fcda4..ffab0aafe59eb90451f1cc6d6be80bbc2db964af 100644
--- a/lib/api/builds.rb
+++ b/lib/api/jobs.rb
@@ -1,5 +1,5 @@
 module API
-  class Builds < Grape::API
+  class Jobs < Grape::API
     include PaginationParams
 
     before { authenticate! }
@@ -7,16 +7,19 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       helpers do
         params :optional_scope do
           optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
-                           values:  ['pending', 'running', 'failed', 'success', 'canceled'],
+                           values: ::CommitStatus::AVAILABLE_STATUSES,
                            coerce_with: ->(scope) {
-                             if scope.is_a?(String)
+                             case scope
+                             when String
                                [scope]
-                             elsif scope.is_a?(Hashie::Mash)
+                             when Hashie::Mash
                                scope.values
+                             when Hashie::Array
+                               scope
                              else
                                ['unknown']
                              end
@@ -24,79 +27,72 @@ module API
         end
       end
 
-      desc 'Get a project builds' do
-        success Entities::Build
+      desc 'Get a projects jobs' do
+        success Entities::Job
       end
       params do
         use :optional_scope
         use :pagination
       end
-      get ':id/builds' do
+      get ':id/jobs' do
         builds = user_project.builds.order('id DESC')
         builds = filter_builds(builds, params[:scope])
 
-        present paginate(builds), with: Entities::Build,
-                                  user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present paginate(builds), with: Entities::Job
       end
 
-      desc 'Get builds for a specific commit of a project' do
-        success Entities::Build
+      desc 'Get pipeline jobs' do
+        success Entities::Job
       end
       params do
-        requires :sha, type: String, desc: 'The SHA id of a commit'
+        requires :pipeline_id, type: Integer,  desc: 'The pipeline ID'
         use :optional_scope
         use :pagination
       end
-      get ':id/repository/commits/:sha/builds' do
-        authorize_read_builds!
-
-        return not_found! unless user_project.commit(params[:sha])
-
-        pipelines = user_project.pipelines.where(sha: params[:sha])
-        builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+      get ':id/pipelines/:pipeline_id/jobs' do
+        pipeline = user_project.pipelines.find(params[:pipeline_id])
+        builds = pipeline.builds
         builds = filter_builds(builds, params[:scope])
 
-        present paginate(builds), with: Entities::Build,
-                                  user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present paginate(builds), with: Entities::Job
       end
 
-      desc 'Get a specific build of a project' do
-        success Entities::Build
+      desc 'Get a specific job of a project' do
+        success Entities::Job
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a job'
       end
-      get ':id/builds/:build_id' do
+      get ':id/jobs/:job_id' do
         authorize_read_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
 
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present build, with: Entities::Job
       end
 
-      desc 'Download the artifacts file from build' do
+      desc 'Download the artifacts file from a job' do
         detail 'This feature was introduced in GitLab 8.5'
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a job'
       end
-      get ':id/builds/:build_id/artifacts' do
+      get ':id/jobs/:job_id/artifacts' do
         authorize_read_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
 
         present_artifacts!(build.artifacts_file)
       end
 
-      desc 'Download the artifacts file from build' do
+      desc 'Download the artifacts file from a job' do
         detail 'This feature was introduced in GitLab 8.10'
       end
       params do
         requires :ref_name, type: String, desc: 'The ref from repository'
-        requires :job,      type: String, desc: 'The name for the build'
+        requires :job,      type: String, desc: 'The name for the job'
       end
-      get ':id/builds/artifacts/:ref_name/download',
+      get ':id/jobs/artifacts/:ref_name/download',
         requirements: { ref_name: /.+/ } do
         authorize_read_builds!
 
@@ -109,14 +105,14 @@ module API
       # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
       #       is saved in the DB instead of file). But before that, we need to consider how to replace the value of
       #       `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
-      desc 'Get a trace of a specific build of a project'
+      desc 'Get a trace of a specific job of a project'
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a job'
       end
-      get ':id/builds/:build_id/trace' do
+      get ':id/jobs/:job_id/trace' do
         authorize_read_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
 
         header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
         content_type 'text/plain'
@@ -126,96 +122,91 @@ module API
         body trace
       end
 
-      desc 'Cancel a specific build of a project' do
-        success Entities::Build
+      desc 'Cancel a specific job of a project' do
+        success Entities::Job
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a job'
       end
-      post ':id/builds/:build_id/cancel' do
+      post ':id/jobs/:job_id/cancel' do
         authorize_update_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
 
         build.cancel
 
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present build, with: Entities::Job
       end
 
       desc 'Retry a specific build of a project' do
-        success Entities::Build
+        success Entities::Job
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a build'
       end
-      post ':id/builds/:build_id/retry' do
+      post ':id/jobs/:job_id/retry' do
         authorize_update_builds!
 
-        build = get_build!(params[:build_id])
-        return forbidden!('Build is not retryable') unless build.retryable?
+        build = get_build!(params[:job_id])
+        return forbidden!('Job is not retryable') unless build.retryable?
 
         build = Ci::Build.retry(build, current_user)
 
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present build, with: Entities::Job
       end
 
-      desc 'Erase build (remove artifacts and build trace)' do
-        success Entities::Build
+      desc 'Erase job (remove artifacts and the trace)' do
+        success Entities::Job
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a build'
       end
-      post ':id/builds/:build_id/erase' do
+      post ':id/jobs/:job_id/erase' do
         authorize_update_builds!
 
-        build = get_build!(params[:build_id])
-        return forbidden!('Build is not erasable!') unless build.erasable?
+        build = get_build!(params[:job_id])
+        return forbidden!('Job is not erasable!') unless build.erasable?
 
         build.erase(erased_by: current_user)
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+        present build, with: Entities::Job
       end
 
       desc 'Keep the artifacts to prevent them from being deleted' do
-        success Entities::Build
+        success Entities::Job
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a build'
+        requires :job_id, type: Integer, desc: 'The ID of a job'
       end
-      post ':id/builds/:build_id/artifacts/keep' do
+      post ':id/jobs/:job_id/artifacts/keep' do
         authorize_update_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
         return not_found!(build) unless build.artifacts?
 
         build.keep_artifacts!
 
         status 200
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present build, with: Entities::Job
       end
 
-      desc 'Trigger a manual build' do
-        success Entities::Build
+      desc 'Trigger a manual job' do
+        success Entities::Job
         detail 'This feature was added in GitLab 8.11'
       end
       params do
-        requires :build_id, type: Integer, desc: 'The ID of a Build'
+        requires :job_id, type: Integer, desc: 'The ID of a Job'
       end
-      post ":id/builds/:build_id/play" do
+      post ":id/jobs/:job_id/play" do
         authorize_read_builds!
 
-        build = get_build!(params[:build_id])
+        build = get_build!(params[:job_id])
 
         bad_request!("Unplayable Job") unless build.playable?
 
         build.play(current_user)
 
         status 200
-        present build, with: Entities::Build,
-                       user_can_download_artifacts: can?(current_user, :read_build, user_project)
+        present build, with: Entities::Job
       end
     end
 
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index d2955af3f95e322beb588e5640d9190d2ceeb1ca..d9a3cb7bb6b57ab51650d18f1fbf17c2804d6c99 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,13 +1,13 @@
 module API
   class Labels < Grape::API
     include PaginationParams
-    
+
     before { authenticate! }
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get all labels of the project' do
         success Entities::Label
       end
@@ -56,7 +56,7 @@ module API
         label = user_project.labels.find_by(title: params[:name])
         not_found!('Label') unless label
 
-        present label.destroy, with: Entities::Label, current_user: current_user, project: user_project
+        label.destroy
       end
 
       desc 'Update an existing label. At least one optional parameter is required.' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d1d78775c6ddaa5973bd3c86228b72918c91b85c..c200e46a3282e1f8de5a74b5eb10c2a8ac08163c 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -10,7 +10,7 @@ module API
       params do
         requires :id, type: String, desc: "The #{source_type} ID"
       end
-      resource source_type.pluralize do
+      resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
         desc 'Gets a list of group or project members viewable by the authenticated user.' do
           success Entities::Member
         end
@@ -55,7 +55,6 @@ module API
           authorize_admin_source!(source_type, source)
 
           member = source.members.find_by(user_id: params[:user_id])
-
           conflict!('Member already exists') if member
 
           member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
@@ -63,9 +62,6 @@ module API
           if member.persisted? && member.valid?
             present member.user, with: Entities::Member, member: member
           else
-            # This is to ensure back-compatibility but 400 behavior should be used
-            # for all validation errors in 9.0!
-            render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
             render_validation_error!(member)
           end
         end
@@ -79,18 +75,14 @@ module API
           optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
         end
         put ":id/members/:user_id" do
-          source = find_source(source_type, params[:id])
+          source = find_source(source_type, params.delete(:id))
           authorize_admin_source!(source_type, source)
 
-          member = source.members.find_by!(user_id: params[:user_id])
-          attrs = attributes_for_keys [:access_level, :expires_at]
+          member = source.members.find_by!(user_id: params.delete(:user_id))
 
-          if member.update_attributes(attrs)
+          if member.update_attributes(declared_params(include_missing: false))
             present member.user, with: Entities::Member, member: member
           else
-            # This is to ensure back-compatibility but 400 behavior should be used
-            # for all validation errors in 9.0!
-            render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
             render_validation_error!(member)
           end
         end
@@ -101,24 +93,10 @@ module API
         end
         delete ":id/members/:user_id" do
           source = find_source(source_type, params[:id])
+          # Ensure that memeber exists
+          source.members.find_by!(user_id: params[:user_id])
 
-          # This is to ensure back-compatibility but find_by! should be used
-          # in that casse in 9.0!
-          member = source.members.find_by(user_id: params[:user_id])
-
-          # This is to ensure back-compatibility but this should be removed in
-          # favor of find_by! in 9.0!
-          not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
-
-          # This is to ensure back-compatibility but 204 behavior should be used
-          # for all DELETE endpoints in 9.0!
-          if member.nil?
-            { message: "Access revoked", id: params[:user_id].to_i }
-          else
-            ::Members::DestroyService.new(source, current_user, declared_params).execute
-
-            present member.user, with: Entities::Member, member: member
-          end
+          ::Members::DestroyService.new(source, current_user, declared_params).execute
         end
       end
     end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 4901a7cfea62d9bc5207b1a9908f4344f024fd46..4b79eac2b8b0d3eac25848dafc18c432fe0b2f2f 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -5,19 +5,21 @@ module API
 
     before { authenticate! }
 
-    resource :projects do
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a list of merge request diff versions' do
         detail 'This feature was introduced in GitLab 8.12.'
         success Entities::MergeRequestDiff
       end
 
       params do
-        requires :id, type: String, desc: 'The ID of a project'
-        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
         use :pagination
       end
-      get ":id/merge_requests/:merge_request_id/versions" do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ":id/merge_requests/:merge_request_iid/versions" do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
 
         present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
       end
@@ -28,13 +30,12 @@ module API
       end
 
       params do
-        requires :id, type: String, desc: 'The ID of a project'
-        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
         requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
       end
 
-      get ":id/merge_requests/:merge_request_id/versions/:version_id" do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
 
         present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
       end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bdd764abfebe50898dfd7fd73af02c6249a88ebc..5cc807d5bffbd7c996de9b8ef5d3256be0a48ec2 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -7,7 +7,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       include TimeTrackingEndpoints
 
       helpers do
@@ -25,6 +25,14 @@ module API
           render_api_error!(errors, 400)
         end
 
+        def issue_entity(project)
+          if project.has_external_issue_tracker?
+            Entities::ExternalIssue
+          else
+            Entities::IssueBasic
+          end
+        end
+
         params :optional_params do
           optional :description, type: String, desc: 'The description of the merge request'
           optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -35,7 +43,7 @@ module API
       end
 
       desc 'List merge requests' do
-        success Entities::MergeRequest
+        success Entities::MergeRequestBasic
       end
       params do
         optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -62,7 +70,7 @@ module API
           end
 
         merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
-        present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+        present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
       end
 
       desc 'Create a merge request' do
@@ -93,23 +101,23 @@ module API
 
       desc 'Delete a merge request'
       params do
-        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
       end
-      delete ":id/merge_requests/:merge_request_id" do
-        merge_request = find_project_merge_request(params[:merge_request_id])
+      delete ":id/merge_requests/:merge_request_iid" do
+        merge_request = find_project_merge_request(params[:merge_request_iid])
 
         authorize!(:destroy_merge_request, merge_request)
         merge_request.destroy
       end
 
       params do
-        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
       end
       desc 'Get a single merge request' do
         success Entities::MergeRequest
       end
-      get ':id/merge_requests/:merge_request_id' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ':id/merge_requests/:merge_request_iid' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
 
         present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
       end
@@ -117,8 +125,8 @@ module API
       desc 'Get the commits of a merge request' do
         success Entities::RepoCommit
       end
-      get ':id/merge_requests/:merge_request_id/commits' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ':id/merge_requests/:merge_request_iid/commits' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
         commits = ::Kaminari.paginate_array(merge_request.commits)
 
         present paginate(commits), with: Entities::RepoCommit
@@ -127,8 +135,8 @@ module API
       desc 'Show the merge request changes' do
         success Entities::MergeRequestChanges
       end
-      get ':id/merge_requests/:merge_request_id/changes' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ':id/merge_requests/:merge_request_iid/changes' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
 
         present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
       end
@@ -146,8 +154,8 @@ module API
                         :milestone_id, :labels, :state_event,
                         :remove_source_branch
       end
-      put ':id/merge_requests/:merge_request_id' do
-        merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+      put ':id/merge_requests/:merge_request_iid' do
+        merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
 
         mr_params = declared_params(include_missing: false)
         mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
@@ -168,12 +176,12 @@ module API
         optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
         optional :should_remove_source_branch, type: Boolean,
                                                desc: 'When true, the source branch will be deleted if possible'
-        optional :merge_when_build_succeeds, type: Boolean,
-                                             desc: 'When true, this merge request will be merged when the pipeline succeeds'
+        optional :merge_when_pipeline_succeeds, type: Boolean,
+                                                desc: 'When true, this merge request will be merged when the pipeline succeeds'
         optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
       end
-      put ':id/merge_requests/:merge_request_id/merge' do
-        merge_request = find_project_merge_request(params[:merge_request_id])
+      put ':id/merge_requests/:merge_request_iid/merge' do
+        merge_request = find_project_merge_request(params[:merge_request_iid])
 
         # Merge request can not be merged
         # because user dont have permissions to push into target branch
@@ -192,7 +200,7 @@ module API
           should_remove_source_branch: params[:should_remove_source_branch]
         }
 
-        if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+        if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
           ::MergeRequests::MergeWhenPipelineSucceedsService
             .new(merge_request.target_project, current_user, merge_params)
             .execute(merge_request)
@@ -208,10 +216,10 @@ module API
       desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
         success Entities::MergeRequest
       end
-      post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do
-        merge_request = find_project_merge_request(params[:merge_request_id])
+      post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+        merge_request = find_project_merge_request(params[:merge_request_iid])
 
-        unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+        unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
 
         ::MergeRequest::MergeWhenPipelineSucceedsService
           .new(merge_request.target_project, current_user)
@@ -224,8 +232,8 @@ module API
       params do
         use :pagination
       end
-      get ':id/merge_requests/:merge_request_id/comments' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ':id/merge_requests/:merge_request_iid/comments' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
         present paginate(merge_request.notes.fresh), with: Entities::MRNote
       end
 
@@ -235,8 +243,8 @@ module API
       params do
         requires :note, type: String, desc: 'The text of the comment'
       end
-      post ':id/merge_requests/:merge_request_id/comments' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+      post ':id/merge_requests/:merge_request_iid/comments' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note)
 
         opts = {
           note: params[:note],
@@ -259,8 +267,8 @@ module API
       params do
         use :pagination
       end
-      get ':id/merge_requests/:merge_request_id/closes_issues' do
-        merge_request = find_merge_request_with_access(params[:merge_request_id])
+      get ':id/merge_requests/:merge_request_iid/closes_issues' do
+        merge_request = find_merge_request_with_access(params[:merge_request_iid])
         issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
         present paginate(issues), with: issue_entity(user_project), current_user: current_user
       end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 0b4ed76b35cab7a349a03778bc79600f35c0fd64..e7ab82f08dbc5a7c30a2d74ec6b1796af41b9019 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -23,14 +23,15 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a list of project milestones' do
         success Entities::Milestone
       end
       params do
         optional :state, type: String, values: %w[active closed all], default: 'all',
                          desc: 'Return "active", "closed", or "all" milestones'
-        optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+        optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+        optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
         use :pagination
       end
       get ":id/milestones" do
@@ -38,7 +39,8 @@ module API
 
         milestones = user_project.milestones
         milestones = filter_milestones_state(milestones, params[:state])
-        milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+        milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
+        milestones = filter_by_search(milestones, params[:search]) if params[:search]
 
         present paginate(milestones), with: Entities::Milestone
       end
@@ -101,7 +103,7 @@ module API
       end
 
       desc 'Get all issues for a single project milestone' do
-        success Entities::Issue
+        success Entities::IssueBasic
       end
       params do
         requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -114,16 +116,17 @@ module API
 
         finder_params = {
           project_id: user_project.id,
-          milestone_title: milestone.title
+          milestone_title: milestone.title,
+          sort: 'position_asc'
         }
 
         issues = IssuesFinder.new(current_user, finder_params).execute
-        present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+        present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
       end
 
       desc 'Get all merge requests for a single project milestone' do
         detail 'This feature was introduced in GitLab 9.'
-        success Entities::MergeRequest
+        success Entities::MergeRequestBasic
       end
       params do
         requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -136,11 +139,15 @@ module API
 
         finder_params = {
           project_id: user_project.id,
-          milestone_id: milestone.id
+          milestone_id: milestone.id,
+          sort: 'position_asc'
         }
 
         merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
-        present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+        present paginate(merge_requests),
+          with: Entities::MergeRequestBasic,
+          current_user: current_user,
+          project: user_project
       end
     end
   end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8beccaaabd1d44689edee10cda84fd85aa14da49..29ceffdbd2d48a0bd647fff85121f98c46e5df5a 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -4,12 +4,12 @@ module API
 
     before { authenticate! }
 
-    NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+    NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       NOTEABLE_TYPES.each do |noteable_type|
         noteables_str = noteable_type.to_s.underscore.pluralize
 
@@ -85,7 +85,7 @@ module API
             note = ::Notes::CreateService.new(user_project, current_user, opts).execute
 
             if note.valid?
-              present note, with: Entities::const_get(note.class.name)
+              present note, with: Entities.const_get(note.class.name)
             else
               not_found!("Note #{note.errors.messages}")
             end
@@ -132,8 +132,6 @@ module API
           authorize! :admin_note, note
 
           ::Notes::DestroyService.new(user_project, current_user).execute(note)
-
-          present note, with: Entities::Note
         end
       end
     end
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index c5e9b3ad69b19ce25f5dca04610fb25d525a2b34..992ea5dc24de8a5cf46be9733c220fa24f98e7b9 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -48,14 +48,14 @@ module API
     end
 
     %w[group project].each do |source_type|
-      resource source_type.pluralize do
+      params do
+        requires :id, type: String, desc: "The #{source_type} ID"
+      end
+      resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
         desc "Get #{source_type} level notification level settings, defaults to Global" do
           detail 'This feature was introduced in GitLab 8.12'
           success Entities::NotificationSetting
         end
-        params do
-          requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
-        end
         get ":id/notification_settings" do
           source = find_source(source_type, params[:id])
 
@@ -69,7 +69,6 @@ module API
           success Entities::NotificationSetting
         end
         params do
-          requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
           optional :level, type: String, desc: "The #{source_type} notification level"
           NotificationSetting::EMAIL_EVENTS.each do |event|
             optional event, type: Boolean, desc: 'Enable/disable this notification'
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index f59f79591738ca96d26d094bcbf2d0777dca676c..754c3d85a04cffdab81ee37f7ddfa129498216c5 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -7,21 +7,21 @@ module API
     params do
       requires :id, type: String, desc: 'The project ID'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get all Pipelines of the project' do
         detail 'This feature was introduced in GitLab 8.11.'
-        success Entities::Pipeline
+        success Entities::PipelineBasic
       end
       params do
         use :pagination
-        optional :scope,    type: String, values: ['running', 'branches', 'tags'],
+        optional :scope,    type: String, values: %w(running branches tags),
                             desc: 'Either running, branches, or tags'
       end
       get ':id/pipelines' do
         authorize! :read_pipeline, user_project
 
         pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
-        present paginate(pipelines), with: Entities::Pipeline
+        present paginate(pipelines), with: Entities::PipelineBasic
       end
 
       desc 'Create a new pipeline' do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index f7a28d7ad10284d3dc6fd85bc66ef2038a3f09f0..53791166c33db4658469b6e444f0e47e9536f8bd 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -24,7 +24,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get project hooks' do
         success Entities::ProjectHook
       end
@@ -90,12 +90,9 @@ module API
         requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
       end
       delete ":id/hooks/:hook_id" do
-        begin
-          present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook
-        rescue
-          # ProjectHook can raise Error if hook_id not found
-          not_found!("Error deleting hook #{params[:hook_id]}")
-        end
+        hook = user_project.hooks.find(params.delete(:hook_id))
+
+        hook.destroy
       end
     end
   end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 2a1cce73f3f834b8c37e5b13549c47abcb0a4b40..cfee38a9bafbd9b292af5b6049b3d8dd52048a18 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -7,7 +7,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       helpers do
         def handle_project_member_errors(errors)
           if errors[:project_access].any?
@@ -50,11 +50,9 @@ module API
         requires :title, type: String, desc: 'The title of the snippet'
         requires :file_name, type: String, desc: 'The file name of the snippet'
         requires :code, type: String, desc: 'The content of the snippet'
-        requires :visibility_level, type: Integer,
-                                    values: [Gitlab::VisibilityLevel::PRIVATE,
-                                             Gitlab::VisibilityLevel::INTERNAL,
-                                             Gitlab::VisibilityLevel::PUBLIC],
-                                    desc: 'The visibility level of the snippet'
+        requires :visibility, type: String,
+                              values: Gitlab::VisibilityLevel.string_values,
+                              desc: 'The visibility of the snippet'
       end
       post ":id/snippets" do
         authorize! :create_project_snippet, user_project
@@ -80,11 +78,9 @@ module API
         optional :title, type: String, desc: 'The title of the snippet'
         optional :file_name, type: String, desc: 'The file name of the snippet'
         optional :code, type: String, desc: 'The content of the snippet'
-        optional :visibility_level, type: Integer,
-                                    values: [Gitlab::VisibilityLevel::PRIVATE,
-                                             Gitlab::VisibilityLevel::INTERNAL,
-                                             Gitlab::VisibilityLevel::PUBLIC],
-                                    desc: 'The visibility level of the snippet'
+        optional :visibility, type: String,
+                              values: Gitlab::VisibilityLevel.string_values,
+                              desc: 'The visibility of the snippet'
         at_least_one_of :title, :file_name, :code, :visibility_level
       end
       put ":id/snippets/:snippet_id" do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 366e5679eddb3eae08d39ac87604b53c54621d6b..0fbe1669d4531e1bba59a42e08980edd6664cd2f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,13 +16,10 @@ module API
         optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
         optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
         optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
-        optional :visibility_level, type: Integer, values: [
-          Gitlab::VisibilityLevel::PRIVATE,
-          Gitlab::VisibilityLevel::INTERNAL,
-          Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+        optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
         optional :public_builds, type: Boolean, desc: 'Perform public builds'
         optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
-        optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+        optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
         optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
       end
     end
@@ -47,11 +44,12 @@ module API
 
         params :filter_params do
           optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
-          optional :visibility, type: String, values: %w[public internal private],
+          optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
                                 desc: 'Limit by visibility'
-          optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+          optional :search, type: String, desc: 'Return list of projects matching the search criteria'
           optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
           optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+          optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
         end
 
         params :statistics_params do
@@ -93,8 +91,9 @@ module API
         success Entities::Project
       end
       params do
-        requires :name, type: String, desc: 'The name of the project'
+        optional :name, type: String, desc: 'The name of the project'
         optional :path, type: String, desc: 'The path of the repository'
+        at_least_one_of :name, :path
         use :optional_params
         use :create_params
       end
@@ -143,7 +142,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects, requirements: { id: /[^\/]+/ } do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a single project' do
         success Entities::ProjectWithAccess
       end
@@ -206,8 +205,8 @@ module API
         at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
                         :wiki_enabled, :builds_enabled, :snippets_enabled,
                         :shared_runners_enabled, :container_registry_enabled,
-                        :lfs_enabled, :visibility_level, :public_builds,
-                        :request_access_enabled, :only_allow_merge_if_build_succeeds,
+                        :lfs_enabled, :visibility, :public_builds,
+                        :request_access_enabled, :only_allow_merge_if_pipeline_succeeds,
                         :only_allow_merge_if_all_discussions_are_resolved, :path,
                         :default_branch
       end
@@ -215,7 +214,7 @@ module API
         authorize_admin_project
         attrs = declared_params(include_missing: false)
         authorize! :rename_project, user_project if attrs[:name].present?
-        authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+        authorize! :change_visibility_level, user_project if attrs[:visibility].present?
 
         result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
 
@@ -281,6 +280,8 @@ module API
       delete ":id" do
         authorize! :remove_project, user_project
         ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+
+        accepted!
       end
 
       desc 'Mark this project as forked from another'
@@ -350,7 +351,6 @@ module API
         not_found!('Group Link') unless link
 
         link.destroy
-        no_content!
       end
 
       desc 'Upload a file'
@@ -374,6 +374,19 @@ module API
 
         present paginate(users), with: Entities::UserBasic
       end
+
+      desc 'Start the housekeeping task for a project' do
+        detail 'This feature was introduced in GitLab 9.0.'
+      end
+      post ':id/housekeeping' do
+        authorize_admin_project
+
+        begin
+          ::Projects::HousekeepingService.new(user_project).execute
+        rescue ::Projects::HousekeepingService::LeaseTaken => error
+          conflict!(error.message)
+        end
+      end
     end
   end
 end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index bfda6f45b0a8d5697bab88b3d37d1a909aa901ce..8f16e532ecbc18f799af07b3c4604e61448b7187 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -9,7 +9,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       helpers do
         def handle_project_member_errors(errors)
           if errors[:project_access].any?
@@ -17,19 +17,34 @@ module API
           end
           not_found!
         end
+
+        def assign_blob_vars!
+          authorize! :download_code, user_project
+
+          @repo = user_project.repository
+
+          begin
+            @blob = Gitlab::Git::Blob.raw(@repo, params[:sha])
+            @blob.load_all_data!(@repo)
+          rescue
+            not_found! 'Blob'
+          end
+
+          not_found! 'Blob' unless @blob
+        end
       end
 
       desc 'Get a project repository tree' do
         success Entities::RepoTreeObject
       end
       params do
-        optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+        optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
         optional :path, type: String, desc: 'The path of the tree'
         optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
         use :pagination
       end
       get ':id/repository/tree' do
-        ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+        ref = params[:ref] || user_project.try(:default_branch) || 'master'
         path = params[:path] || nil
 
         commit = user_project.commit(ref)
@@ -40,39 +55,29 @@ module API
         present paginate(entries), with: Entities::RepoTreeObject
       end
 
-      desc 'Get a raw file contents'
+      desc 'Get raw blob contents from the repository'
       params do
         requires :sha, type: String, desc: 'The commit, branch name, or tag name'
-        requires :filepath, type: String, desc: 'The path to the file to display'
       end
-      get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do
-        repo = user_project.repository
-
-        commit = repo.commit(params[:sha])
-        not_found! "Commit" unless commit
+      get ':id/repository/blobs/:sha/raw' do
+        assign_blob_vars!
 
-        blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
-        not_found! "File" unless blob
-
-        send_git_blob repo, blob
+        send_git_blob @repo, @blob
       end
 
-      desc 'Get a raw blob contents by blob sha'
+      desc 'Get a blob from the repository'
       params do
         requires :sha, type: String, desc: 'The commit, branch name, or tag name'
       end
-      get ':id/repository/raw_blobs/:sha' do
-        repo = user_project.repository
-
-        begin
-          blob = Gitlab::Git::Blob.raw(repo, params[:sha])
-        rescue
-          not_found! 'Blob'
-        end
-
-        not_found! 'Blob' unless blob
+      get ':id/repository/blobs/:sha' do
+        assign_blob_vars!
 
-        send_git_blob repo, blob
+        {
+          size: @blob.size,
+          encoding: "base64",
+          content: Base64.strict_encode64(@blob.data),
+          sha: @blob.id
+        }
       end
 
       desc 'Get an archive of the repository'
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4c9db2c87161591bbd0f0f2bbf984d34663ff024
--- /dev/null
+++ b/lib/api/runner.rb
@@ -0,0 +1,264 @@
+module API
+  class Runner < Grape::API
+    helpers ::API::Helpers::Runner
+
+    resource :runners do
+      desc 'Registers a new Runner' do
+        success Entities::RunnerRegistrationDetails
+        http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: 'Registration token'
+        optional :description, type: String, desc: %q(Runner's description)
+        optional :info, type: Hash, desc: %q(Runner's metadata)
+        optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+        optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
+        optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+      end
+      post '/' do
+        attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+
+        runner =
+          if runner_registration_token_valid?
+            # Create shared runner. Requires admin access
+            Ci::Runner.create(attributes.merge(is_shared: true))
+          elsif project = Project.find_by(runners_token: params[:token])
+            # Create a specific runner for project.
+            project.runners.create(attributes)
+          end
+
+        return forbidden! unless runner
+
+        if runner.id
+          runner.update(get_runner_version_from_params)
+          present runner, with: Entities::RunnerRegistrationDetails
+        else
+          not_found!
+        end
+      end
+
+      desc 'Deletes a registered Runner' do
+        http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: %q(Runner's authentication token)
+      end
+      delete '/' do
+        authenticate_runner!
+        Ci::Runner.find_by_token(params[:token]).destroy
+      end
+
+      desc 'Validates authentication credentials' do
+        http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: %q(Runner's authentication token)
+      end
+      post '/verify' do
+        authenticate_runner!
+        status 200
+      end
+    end
+
+    resource :jobs do
+      desc 'Request a job' do
+        success Entities::JobRequest::Response
+        http_codes [[201, 'Job was scheduled'],
+                    [204, 'No job for Runner'],
+                    [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: %q(Runner's authentication token)
+        optional :last_update, type: String, desc: %q(Runner's queue last_update token)
+        optional :info, type: Hash, desc: %q(Runner's metadata)
+      end
+      post '/request' do
+        authenticate_runner!
+        no_content! unless current_runner.active?
+        update_runner_info
+
+        if current_runner.is_runner_queue_value_latest?(params[:last_update])
+          header 'X-GitLab-Last-Update', params[:last_update]
+          Gitlab::Metrics.add_event(:build_not_found_cached)
+          return no_content!
+        end
+
+        new_update = current_runner.ensure_runner_queue_value
+        result = ::Ci::RegisterJobService.new(current_runner).execute
+
+        if result.valid?
+          if result.build
+            Gitlab::Metrics.add_event(:build_found,
+                                      project: result.build.project.path_with_namespace)
+            present result.build, with: Entities::JobRequest::Response
+          else
+            Gitlab::Metrics.add_event(:build_not_found)
+            header 'X-GitLab-Last-Update', new_update
+            no_content!
+          end
+        else
+          # We received build that is invalid due to concurrency conflict
+          Gitlab::Metrics.add_event(:build_invalid)
+          conflict!
+        end
+      end
+
+      desc 'Updates a job' do
+        http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: %q(Runners's authentication token)
+        requires :id, type: Integer, desc: %q(Job's ID)
+        optional :trace, type: String, desc: %q(Job's full trace)
+        optional :state, type: String, desc: %q(Job's status: success, failed)
+      end
+      put '/:id' do
+        job = Ci::Build.find_by_id(params[:id])
+        authenticate_job!(job)
+
+        job.update_attributes(trace: params[:trace]) if params[:trace]
+
+        Gitlab::Metrics.add_event(:update_build,
+                                  project: job.project.path_with_namespace)
+
+        case params[:state].to_s
+        when 'success'
+          job.success
+        when 'failed'
+          job.drop
+        end
+      end
+
+      desc 'Appends a patch to the job trace' do
+        http_codes [[202, 'Trace was patched'],
+                    [400, 'Missing Content-Range header'],
+                    [403, 'Forbidden'],
+                    [416, 'Range not satisfiable']]
+      end
+      params do
+        requires :id, type: Integer, desc: %q(Job's ID)
+        optional :token, type: String, desc: %q(Job's authentication token)
+      end
+      patch '/:id/trace' do
+        job = Ci::Build.find_by_id(params[:id])
+        authenticate_job!(job)
+
+        error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+        content_range = request.headers['Content-Range']
+        content_range = content_range.split('-')
+
+        current_length = job.trace_length
+        unless current_length == content_range[0].to_i
+          return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" })
+        end
+
+        job.append_trace(request.body.read, content_range[0].to_i)
+
+        status 202
+        header 'Job-Status', job.status
+        header 'Range', "0-#{job.trace_length}"
+      end
+
+      desc 'Authorize artifacts uploading for job' do
+        http_codes [[200, 'Upload allowed'],
+                    [403, 'Forbidden'],
+                    [405, 'Artifacts support not enabled'],
+                    [413, 'File too large']]
+      end
+      params do
+        requires :id, type: Integer, desc: %q(Job's ID)
+        optional :token, type: String, desc: %q(Job's authentication token)
+        optional :filesize, type: Integer, desc: %q(Artifacts filesize)
+      end
+      post '/:id/artifacts/authorize' do
+        not_allowed! unless Gitlab.config.artifacts.enabled
+        require_gitlab_workhorse!
+        Gitlab::Workhorse.verify_api_request!(headers)
+
+        job = Ci::Build.find_by_id(params[:id])
+        authenticate_job!(job)
+        forbidden!('Job is not running') unless job.running?
+
+        if params[:filesize]
+          file_size = params[:filesize].to_i
+          file_to_large! unless file_size < max_artifacts_size
+        end
+
+        status 200
+        content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+        Gitlab::Workhorse.artifact_upload_ok
+      end
+
+      desc 'Upload artifacts for job' do
+        success Entities::JobRequest::Response
+        http_codes [[201, 'Artifact uploaded'],
+                    [400, 'Bad request'],
+                    [403, 'Forbidden'],
+                    [405, 'Artifacts support not enabled'],
+                    [413, 'File too large']]
+      end
+      params do
+        requires :id, type: Integer, desc: %q(Job's ID)
+        optional :token, type: String, desc: %q(Job's authentication token)
+        optional :expire_in, type: String, desc: %q(Specify when artifacts should expire)
+        optional :file, type: File, desc: %q(Artifact's file)
+        optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+        optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
+        optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
+        optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+        optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
+      end
+      post '/:id/artifacts' do
+        not_allowed! unless Gitlab.config.artifacts.enabled
+        require_gitlab_workhorse!
+
+        job = Ci::Build.find_by_id(params[:id])
+        authenticate_job!(job)
+        forbidden!('Job is not running!') unless job.running?
+
+        artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+        artifacts = uploaded_file(:file, artifacts_upload_path)
+        metadata = uploaded_file(:metadata, artifacts_upload_path)
+
+        bad_request!('Missing artifacts file!') unless artifacts
+        file_to_large! unless artifacts.size < max_artifacts_size
+
+        job.artifacts_file = artifacts
+        job.artifacts_metadata = metadata
+        job.artifacts_expire_in = params['expire_in'] ||
+          Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
+
+        if job.save
+          present job, with: Entities::JobRequest::Response
+        else
+          render_validation_error!(job)
+        end
+      end
+
+      desc 'Download the artifacts file for job' do
+        http_codes [[200, 'Upload allowed'],
+                    [403, 'Forbidden'],
+                    [404, 'Artifact not found']]
+      end
+      params do
+        requires :id, type: Integer, desc: %q(Job's ID)
+        optional :token, type: String, desc: %q(Job's authentication token)
+      end
+      get '/:id/artifacts' do
+        job = Ci::Build.find_by_id(params[:id])
+        authenticate_job!(job)
+
+        artifacts_file = job.artifacts_file
+        unless artifacts_file.file_storage?
+          return redirect_to job.artifacts_file.url
+        end
+
+        unless artifacts_file.exists?
+          not_found!
+        end
+
+        present_file!(artifacts_file.path, artifacts_file.filename)
+      end
+    end
+  end
+end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4fbd40965335740718b7ac352b879e6456f81cea..a77c876a749ee44060c47ec9b44e19d396ad5c62 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
         use :pagination
       end
       get do
-        runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
+        runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
         present paginate(runners), with: Entities::Runner
       end
 
@@ -78,16 +78,15 @@ module API
       delete ':id' do
         runner = get_runner(params[:id])
         authenticate_delete_runner!(runner)
-        runner.destroy!
 
-        present runner, with: Entities::Runner
+        runner.destroy!
       end
     end
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       before { authorize_admin_project }
 
       desc 'Get runners available for project' do
@@ -136,8 +135,6 @@ module API
         forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
 
         runner_project.destroy
-
-        present runner, with: Entities::Runner
       end
     end
 
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1456fe4688ba88a5f368604945617e7c74f6cb8a..4e0c9cb1f63c20a783e06475be9d3f53d435082a 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
           desc: 'Enable SSL verification for communication'
         }
       ],
-      'builds-email' => [
-        {
-          required: true,
-          name: :recipients,
-          type: String,
-          desc: 'Comma-separated list of recipient email addresses'
-        },
-        {
-          required: false,
-          name: :add_pusher,
-          type: Boolean,
-          desc: 'Add pusher to recipients list'
-        },
-        {
-          required: false,
-          name: :notify_only_broken_builds,
-          type: Boolean,
-          desc: 'Notify only broken builds'
-        }
-      ],
       'campfire' => [
         {
           required: true,
@@ -403,9 +383,9 @@ module API
         },
         {
           required: false,
-          name: :notify_only_broken_builds,
+          name: :notify_only_broken_pipelines,
           type: Boolean,
-          desc: 'Notify only broken builds'
+          desc: 'Notify only broken pipelines'
         }
       ],
       'pivotaltracker' => [
@@ -422,6 +402,14 @@ module API
           desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
         }
       ],
+      'prometheus' => [
+        {
+          required: true,
+          name: :api_url,
+          type: String,
+          desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
+        }
+      ],
       'pushover' => [
         {
           required: true,
@@ -542,7 +530,6 @@ module API
       BambooService,
       BugzillaService,
       BuildkiteService,
-      BuildsEmailService,
       CampfireService,
       CustomIssueTrackerService,
       DroneCiService,
@@ -558,12 +545,26 @@ module API
       SlackSlashCommandsService,
       PipelinesEmailService,
       PivotaltrackerService,
+      PrometheusService,
       PushoverService,
       RedmineService,
       SlackService,
       MattermostService,
       TeamcityService,
-    ].freeze
+    ]
+
+    if Rails.env.development?
+      services['mock-ci'] = [
+        {
+          required: true,
+          name: :mock_service_url,
+          type: String,
+          desc: 'URL to the mock service'
+        }
+      ]
+
+      service_classes << MockCiService
+    end
 
     trigger_services = {
       'mattermost-slash-commands' => [
@@ -582,7 +583,10 @@ module API
       ]
     }.freeze
 
-    resource :projects do
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
       before { authenticate! }
       before { authorize_admin_project }
 
@@ -598,7 +602,7 @@ module API
         desc "Set #{service_slug} service for project"
         params do
           service_classes.each do |service|
-            event_names = service.try(:event_names) || []
+            event_names = service.try(:event_names) || next
             event_names.each do |event_name|
               services[service.to_param.tr("_", "-")] << {
                 required: false,
@@ -641,9 +645,7 @@ module API
           hash.merge!(key => nil)
         end
 
-        if service.update_attributes(attrs.merge(active: false))
-          true
-        else
+        unless service.update_attributes(attrs.merge(active: false))
           render_api_error!('400 Bad Request', 400)
         end
       end
@@ -672,7 +674,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc "Trigger a slash command for #{service_slug}" do
           detail 'Added in GitLab 8.13'
         end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 747ceb4e3e02d86ce9b7ea434660b6488d8042ff..d4d3229f0d1f1006260adcdb21a90cbcbff7706b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -21,9 +21,9 @@ module API
     end
     params do
       optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
-      optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
-      optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
-      optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+      optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
+      optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
+      optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
       optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
       optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
                                 desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
@@ -56,7 +56,8 @@ module API
       given shared_runners_enabled: ->(val) { val } do
         requires :shared_runners_text, type: String, desc: 'Shared runners text '
       end
-      optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+      optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
+      optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
       optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
       optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
       optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
@@ -117,7 +118,9 @@ module API
                       :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
                       :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
                       :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
-                      :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+                      :shared_runners_enabled, :max_artifacts_size,
+                      :default_artifacts_expire_in, :max_pages_size,
+                      :container_registry_token_expire_delay,
                       :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
                       :akismet_enabled, :admin_notification_email, :sentry_enabled,
                       :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
@@ -125,7 +128,9 @@ module API
                       :housekeeping_enabled, :terminal_max_session_time
     end
     put "application/settings" do
-      if current_settings.update_attributes(declared_params(include_missing: false))
+      attrs = declared_params(include_missing: false)
+
+      if current_settings.update_attributes(attrs)
         present current_settings, with: Entities::ApplicationSetting
       else
         render_validation_error!(current_settings)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index ac03fbd2a3dc62f572eb4837731ac7131ca2801d..b93fdc6280801441351290168a266113161bb62f 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -58,10 +58,10 @@ module API
         requires :title, type: String, desc: 'The title of a snippet'
         requires :file_name, type: String, desc: 'The name of a snippet file'
         requires :content, type: String, desc: 'The content of a snippet'
-        optional :visibility_level, type: Integer,
-                                    values: Gitlab::VisibilityLevel.values,
-                                    default: Gitlab::VisibilityLevel::INTERNAL,
-                                    desc: 'The visibility level of the snippet'
+        optional :visibility, type: String,
+                              values: Gitlab::VisibilityLevel.string_values,
+                              default: 'internal',
+                              desc: 'The visibility of the snippet'
       end
       post do
         attrs = declared_params(include_missing: false).merge(request: request, api: true)
@@ -85,10 +85,10 @@ module API
         optional :title, type: String, desc: 'The title of a snippet'
         optional :file_name, type: String, desc: 'The name of a snippet file'
         optional :content, type: String, desc: 'The content of a snippet'
-        optional :visibility_level, type: Integer,
-                                    values: Gitlab::VisibilityLevel.values,
-                                    desc: 'The visibility level of the snippet'
-        at_least_one_of :title, :file_name, :content, :visibility_level
+        optional :visibility, type: String,
+                              values: Gitlab::VisibilityLevel.string_values,
+                              desc: 'The visibility of the snippet'
+        at_least_one_of :title, :file_name, :content, :visibility
       end
       put ':id' do
         snippet = snippets_for_current_user.find_by(id: params.delete(:id))
@@ -118,9 +118,10 @@ module API
       delete ':id' do
         snippet = snippets_for_current_user.find_by(id: params.delete(:id))
         return not_found!('Snippet') unless snippet
+
         authorize! :destroy_personal_snippet, snippet
+
         snippet.destroy
-        no_content!
       end
 
       desc 'Get a raw snippet' do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index acf11dbdf26ae819e2f24a52bd637be26435c3f5..dbe54d3cd31af02c49a4dba764e676894ac4bbca 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,7 +3,6 @@ module API
     before { authenticate! }
 
     subscribable_types = {
-      'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
       'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
       'issues' => proc { |id| find_project_issue(id) },
       'labels' => proc { |id| find_project_label(id) },
@@ -13,7 +12,7 @@ module API
       requires :id, type: String, desc: 'The ID of a project'
       requires :subscribable_id, type: String, desc: 'The ID of a resource'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       subscribable_types.each do |type, finder|
         type_singularized = type.singularize
         entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index d038a3fa828321257e31f7d654adeaf4a5cabc78..ed7b23b474a702dc52172b2ed530dc34eb0eac8b 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -66,7 +66,7 @@ module API
         hook = SystemHook.find_by(id: params[:id])
         not_found!('System hook') unless hook
 
-        present hook.destroy, with: Entities::Hook
+        hook.destroy
       end
     end
   end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 86759ab882f72f730333d0a7029d4fe5968c60c4..c7b1efe0bfa988d37844a6ef82dd62297c697268 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -7,7 +7,7 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get a project repository tags' do
         success Entities::RepoTag
       end
@@ -66,11 +66,7 @@ module API
         result = ::Tags::DestroyService.new(user_project, current_user).
           execute(params[:tag_name])
 
-        if result[:status] == :success
-          {
-            tag_name: params[:tag_name]
-          }
-        else
+        if result[:status] != :success
           render_api_error!(result[:message], result[:return_code])
         end
       end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index 85b5f7d98b8d19ea793dd9b88121a22426eb91a9..05b4b490e271a2ca2fc476f910585eb1ca0f42b5 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -5,11 +5,11 @@ module API
     included do
       helpers do
         def issuable_name
-          declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+          declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request'
         end
 
         def issuable_key
-          "#{issuable_name}_id".to_sym
+          "#{issuable_name}_iid".to_sym
         end
 
         def update_issuable_key
@@ -50,7 +50,7 @@ module API
 
       issuable_name            = name.end_with?('Issues') ? 'issue' : 'merge_request'
       issuable_collection_name = issuable_name.pluralize
-      issuable_key             = "#{issuable_name}_id".to_sym
+      issuable_key             = "#{issuable_name}_iid".to_sym
 
       desc "Set a time estimate for a project #{issuable_name}"
       params do
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 0b9650b296ce96b84feaf0466e88abdc153fb5e4..d1f7e364029c8fc9bc751d2f7e98768ce778dd33 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,22 +5,22 @@ module API
     before { authenticate! }
 
     ISSUABLE_TYPES = {
-      'merge_requests' => ->(id) { find_merge_request_with_access(id) },
-      'issues' => ->(id) { find_project_issue(id) }
-    }
+      'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
+      'issues' => ->(iid) { find_project_issue(iid) }
+    }.freeze
 
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       ISSUABLE_TYPES.each do |type, finder|
-        type_id_str = "#{type.singularize}_id".to_sym
+        type_id_str = "#{type.singularize}_iid".to_sym
 
         desc 'Create a todo on an issuable' do
           success Entities::Todo
         end
         params do
-          requires type_id_str, type: Integer, desc: 'The ID of an issuable'
+          requires type_id_str, type: Integer, desc: 'The IID of an issuable'
         end
         post ":id/#{type}/:#{type_id_str}/todo" do
           issuable = instance_exec(params[type_id_str], &finder)
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 87a717ba751257a65d4eb75793957e7f0427fda0..a9f2ca2608ea526c3b395fa96daaaee5b9e53c01 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -5,38 +5,33 @@ module API
     params do
       requires :id, type: String, desc: 'The ID of a project'
     end
-    resource :projects do
-      desc 'Trigger a GitLab project build' do
-        success Entities::TriggerRequest
+    resource :projects, requirements: { id: %r{[^/]+} } do
+      desc 'Trigger a GitLab project pipeline' do
+        success Entities::Pipeline
       end
       params do
         requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
         requires :token, type: String, desc: 'The unique token of trigger'
         optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
       end
-      post ":id/(ref/:ref/)trigger/builds" do
+      post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
         project = find_project(params[:id])
         trigger = Ci::Trigger.find_by_token(params[:token].to_s)
         not_found! unless project && trigger
         unauthorized! unless trigger.project == project
 
         # validate variables
-        variables = params[:variables]
-        if variables
-          unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
-            render_api_error!('variables needs to be a map of key-valued strings', 400)
-          end
-
-          # convert variables from Mash to Hash
-          variables = variables.to_h
+        variables = params[:variables].to_h
+        unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+          render_api_error!('variables needs to be a map of key-valued strings', 400)
         end
 
         # create request and trigger builds
         trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
         if trigger_request
-          present trigger_request, with: Entities::TriggerRequest
+          present trigger_request.pipeline, with: Entities::Pipeline
         else
-          errors = 'No builds created'
+          errors = 'No pipeline created'
           render_api_error!(errors, 400)
         end
       end
@@ -60,13 +55,13 @@ module API
         success Entities::Trigger
       end
       params do
-        requires :token, type: String, desc: 'The unique token of trigger'
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
       end
-      get ':id/triggers/:token' do
+      get ':id/triggers/:trigger_id' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.find_by(token: params[:token].to_s)
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
         return not_found!('Trigger') unless trigger
 
         present trigger, with: Entities::Trigger
@@ -75,31 +70,79 @@ module API
       desc 'Create a trigger' do
         success Entities::Trigger
       end
+      params do
+        requires :description, type: String,  desc: 'The trigger description'
+      end
       post ':id/triggers' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.create
+        trigger = user_project.triggers.create(
+          declared_params(include_missing: false).merge(owner: current_user))
 
-        present trigger, with: Entities::Trigger
+        if trigger.valid?
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
+      end
+
+      desc 'Update a trigger' do
+        success Entities::Trigger
+      end
+      params do
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
+        optional :description, type: String,  desc: 'The trigger description'
+      end
+      put ':id/triggers/:trigger_id' do
+        authenticate!
+        authorize! :admin_build, user_project
+
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
+        return not_found!('Trigger') unless trigger
+
+        if trigger.update(declared_params(include_missing: false))
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
+      end
+
+      desc 'Take ownership of trigger' do
+        success Entities::Trigger
+      end
+      params do
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
+      end
+      post ':id/triggers/:trigger_id/take_ownership' do
+        authenticate!
+        authorize! :admin_build, user_project
+
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
+        return not_found!('Trigger') unless trigger
+
+        if trigger.update(owner: current_user)
+          status :ok
+          present trigger, with: Entities::Trigger
+        else
+          render_validation_error!(trigger)
+        end
       end
 
       desc 'Delete a trigger' do
         success Entities::Trigger
       end
       params do
-        requires :token, type: String, desc: 'The unique token of trigger'
+        requires :trigger_id, type: Integer,  desc: 'The trigger ID'
       end
-      delete ':id/triggers/:token' do
+      delete ':id/triggers/:trigger_id' do
         authenticate!
         authorize! :admin_build, user_project
 
-        trigger = user_project.triggers.find_by(token: params[:token].to_s)
+        trigger = user_project.triggers.find(params.delete(:trigger_id))
         return not_found!('Trigger') unless trigger
 
         trigger.destroy
-
-        present trigger, with: Entities::Trigger
       end
     end
   end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index fbc179536912b77a6f66a2c6b2b75f270d61050b..2d4d5a25221eeaae6e99d74bb89dd0651b2929f2 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -9,6 +9,11 @@ module API
 
     resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
       helpers do
+        def find_user(params)
+          id = params[:user_id] || params[:id]
+          User.find_by(id: id) || not_found!('User')
+        end
+
         params :optional_attributes do
           optional :skype, type: String, desc: 'The Skype username'
           optional :linkedin, type: String, desc: 'The LinkedIn username'
@@ -40,7 +45,7 @@ module API
         use :pagination
       end
       get do
-        unless can?(current_user, :read_users_list, nil)
+        unless can?(current_user, :read_users_list)
           render_api_error!("Not authorized.", 403)
         end
 
@@ -172,7 +177,7 @@ module API
           end
         end
 
-        user_params.merge!(password_expires_at: Time.now) if user_params[:password].present?
+        user_params[:password_expires_at] = Time.now if user_params[:password].present?
 
         if user.update_attributes(user_params.except(:extern_uid, :provider))
           present user, with: Entities::UserPublic
@@ -236,7 +241,7 @@ module API
         key = user.keys.find_by(id: params[:key_id])
         not_found!('Key') unless key
 
-        present key.destroy, with: Entities::SSHKey
+        key.destroy
       end
 
       desc 'Add an email address to a specified user. Available only for admins.' do
@@ -362,6 +367,76 @@ module API
 
         present paginate(events), with: Entities::Event
       end
+
+      params do
+        requires :user_id, type: Integer, desc: 'The ID of the user'
+      end
+      segment ':user_id' do
+        resource :impersonation_tokens do
+          helpers do
+            def finder(options = {})
+              user = find_user(params)
+              PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
+            end
+
+            def find_impersonation_token
+              finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
+            end
+          end
+
+          before { authenticated_as_admin! }
+
+          desc 'Retrieve impersonation tokens. Available only for admins.' do
+            detail 'This feature was introduced in GitLab 9.0'
+            success Entities::ImpersonationToken
+          end
+          params do
+            use :pagination
+            optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens'
+          end
+          get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken }
+
+          desc 'Create a impersonation token. Available only for admins.' do
+            detail 'This feature was introduced in GitLab 9.0'
+            success Entities::ImpersonationToken
+          end
+          params do
+            requires :name, type: String, desc: 'The name of the impersonation token'
+            optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token'
+            optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token'
+          end
+          post do
+            impersonation_token = finder.build(declared_params(include_missing: false))
+
+            if impersonation_token.save
+              present impersonation_token, with: Entities::ImpersonationToken
+            else
+              render_validation_error!(impersonation_token)
+            end
+          end
+
+          desc 'Retrieve impersonation token. Available only for admins.' do
+            detail 'This feature was introduced in GitLab 9.0'
+            success Entities::ImpersonationToken
+          end
+          params do
+            requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+          end
+          get ':impersonation_token_id' do
+            present find_impersonation_token, with: Entities::ImpersonationToken
+          end
+
+          desc 'Revoke a impersonation token. Available only for admins.' do
+            detail 'This feature was introduced in GitLab 9.0'
+          end
+          params do
+            requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+          end
+          delete ':impersonation_token_id' do
+            find_impersonation_token.revoke!
+          end
+        end
+      end
     end
 
     resource :user do
@@ -422,7 +497,7 @@ module API
         key = current_user.keys.find_by(id: params[:key_id])
         not_found!('Key') unless key
 
-        present key.destroy, with: Entities::SSHKey
+        key.destroy
       end
 
       desc "Get the currently authenticated user's email addresses" do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b96b2d70b1290f27b409ded63e54eb15d9a792af
--- /dev/null
+++ b/lib/api/v3/award_emoji.rb
@@ -0,0 +1,130 @@
+module API
+  module V3
+    class AwardEmoji < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+      AWARDABLES = %w[issue merge_request snippet].freeze
+
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        AWARDABLES.each do |awardable_type|
+          awardable_string = awardable_type.pluralize
+          awardable_id_string = "#{awardable_type}_id"
+
+          params do
+            requires :id, type: String, desc: 'The ID of a project'
+            requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
+          end
+
+          [
+            ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+            ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
+          ].each do |endpoint|
+
+            desc 'Get a list of project +awardable+ award emoji' do
+              detail 'This feature was introduced in 8.9'
+              success Entities::AwardEmoji
+            end
+            params do
+              use :pagination
+            end
+            get endpoint do
+              if can_read_awardable?
+                awards = awardable.award_emoji
+                present paginate(awards), with: Entities::AwardEmoji
+              else
+                not_found!("Award Emoji")
+              end
+            end
+
+            desc 'Get a specific award emoji' do
+              detail 'This feature was introduced in 8.9'
+              success Entities::AwardEmoji
+            end
+            params do
+              requires :award_id, type: Integer, desc: 'The ID of the award'
+            end
+            get "#{endpoint}/:award_id" do
+              if can_read_awardable?
+                present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
+              else
+                not_found!("Award Emoji")
+              end
+            end
+
+            desc 'Award a new Emoji' do
+              detail 'This feature was introduced in 8.9'
+              success Entities::AwardEmoji
+            end
+            params do
+              requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
+            end
+            post endpoint do
+              not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
+
+              award = awardable.create_award_emoji(params[:name], current_user)
+
+              if award.persisted?
+                present award, with: Entities::AwardEmoji
+              else
+                not_found!("Award Emoji #{award.errors.messages}")
+              end
+            end
+
+            desc 'Delete a +awardables+ award emoji' do
+              detail 'This feature was introduced in 8.9'
+              success Entities::AwardEmoji
+            end
+            params do
+              requires :award_id, type: Integer, desc: 'The ID of an award emoji'
+            end
+            delete "#{endpoint}/:award_id" do
+              award = awardable.award_emoji.find(params[:award_id])
+
+              unauthorized! unless award.user == current_user || current_user.admin?
+
+              award.destroy
+              present award, with: Entities::AwardEmoji
+            end
+          end
+        end
+      end
+
+      helpers do
+        def can_read_awardable?
+          can?(current_user, read_ability(awardable), awardable)
+        end
+
+        def can_award_awardable?
+          awardable.user_can_award?(current_user, params[:name])
+        end
+
+        def awardable
+          @awardable ||=
+            begin
+              if params.include?(:note_id)
+                note_id = params.delete(:note_id)
+
+                awardable.notes.find(note_id)
+              elsif params.include?(:issue_id)
+                user_project.issues.find(params[:issue_id])
+              elsif params.include?(:merge_request_id)
+                user_project.merge_requests.find(params[:merge_request_id])
+              else
+                user_project.snippets.find(params[:snippet_id])
+              end
+            end
+        end
+
+        def read_ability(awardable)
+          case awardable
+          when Note
+            read_ability(awardable.noteable)
+          else
+            :"read_#{awardable.class.to_s.underscore}"
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
index 31d708bc2c83a6b3c5cad7024de7ed1ff051f61a..94acc67171e045b600e63aebceb02ce99281a530 100644
--- a/lib/api/v3/boards.rb
+++ b/lib/api/v3/boards.rb
@@ -6,7 +6,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get all project boards' do
           detail 'This feature was introduced in 8.13'
           success ::API::Entities::Board
@@ -44,6 +44,27 @@ module API
             authorize!(:read_board, user_project)
             present board_lists, with: ::API::Entities::List
           end
+
+          desc 'Delete a board list' do
+            detail 'This feature was introduced in 8.13'
+            success ::API::Entities::List
+          end
+          params do
+            requires :list_id, type: Integer, desc: 'The ID of a board list'
+          end
+          delete "/lists/:list_id" do
+            authorize!(:admin_list, user_project)
+
+            list = board_lists.find(params[:list_id])
+
+            service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+
+            if service.execute(list)
+              present list, with: ::API::Entities::List
+            else
+              render_api_error!({ error: 'List could not be deleted!' }, 400)
+            end
+          end
         end
       end
     end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 733c6b21be5d50124b6831c0b331761b7d2e9a97..0a877b960f6684bfcf9a1e23c14f88ccd2817d1d 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -9,7 +9,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get a project repository branches' do
           success ::API::Entities::RepoBranch
         end
@@ -18,6 +18,54 @@ module API
 
           present branches, with: ::API::Entities::RepoBranch, project: user_project
         end
+
+        desc 'Delete a branch'
+        params do
+          requires :branch, type: String, desc: 'The name of the branch'
+        end
+        delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
+          authorize_push_project
+
+          result = DeleteBranchService.new(user_project, current_user).
+                   execute(params[:branch])
+
+          if result[:status] == :success
+            status(200)
+            {
+              branch_name: params[:branch]
+            }
+          else
+            render_api_error!(result[:message], result[:return_code])
+          end
+        end
+
+        desc 'Delete all merged branches'
+        delete ":id/repository/merged_branches" do
+          DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+          status(200)
+        end
+
+        desc 'Create branch' do
+          success ::API::Entities::RepoBranch
+        end
+        params do
+          requires :branch_name, type: String, desc: 'The name of the branch'
+          requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
+        end
+        post ":id/repository/branches" do
+          authorize_push_project
+          result = CreateBranchService.new(user_project, current_user).
+            execute(params[:branch_name], params[:ref])
+
+          if result[:status] == :success
+            present result[:branch],
+              with: ::API::Entities::RepoBranch,
+              project: user_project
+          else
+            render_api_error!(result[:message], 400)
+          end
+        end
       end
     end
   end
diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..417e4ad0b263d232ac7b24de127571dc14720122
--- /dev/null
+++ b/lib/api/v3/broadcast_messages.rb
@@ -0,0 +1,31 @@
+module API
+  module V3
+    class BroadcastMessages < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+      before { authenticated_as_admin! }
+
+      resource :broadcast_messages do
+        helpers do
+          def find_message
+            BroadcastMessage.find(params[:id])
+          end
+        end
+
+        desc 'Delete a broadcast message' do
+          detail 'This feature was introduced in GitLab 8.12.'
+          success ::API::Entities::BroadcastMessage
+        end
+        params do
+          requires :id, type: Integer, desc: 'Broadcast message ID'
+        end
+        delete ':id' do
+          message = find_message
+
+          present message.destroy, with: ::API::Entities::BroadcastMessage
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f97102c6ef172d48ac52ec2e8dfb0f63ec62278
--- /dev/null
+++ b/lib/api/v3/builds.rb
@@ -0,0 +1,255 @@
+module API
+  module V3
+    class Builds < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        helpers do
+          params :optional_scope do
+            optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+                             values:  %w(pending running failed success canceled skipped),
+                             coerce_with: ->(scope) {
+                                            if scope.is_a?(String)
+                                              [scope]
+                                            elsif   scope.is_a?(Hashie::Mash)
+                                              scope.values
+                                            else
+                                              ['unknown']
+                                            end
+                                          }
+          end
+        end
+
+        desc 'Get a project builds' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          use :optional_scope
+          use :pagination
+        end
+        get ':id/builds' do
+          builds = user_project.builds.order('id DESC')
+          builds = filter_builds(builds, params[:scope])
+
+          present paginate(builds), with: ::API::V3::Entities::Build
+        end
+
+        desc 'Get builds for a specific commit of a project' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :sha, type: String, desc: 'The SHA id of a commit'
+          use :optional_scope
+          use :pagination
+        end
+        get ':id/repository/commits/:sha/builds' do
+          authorize_read_builds!
+
+          return not_found! unless user_project.commit(params[:sha])
+
+          pipelines = user_project.pipelines.where(sha: params[:sha])
+          builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+          builds = filter_builds(builds, params[:scope])
+
+          present paginate(builds), with: ::API::V3::Entities::Build
+        end
+
+        desc 'Get a specific build of a project' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        get ':id/builds/:build_id' do
+          authorize_read_builds!
+
+          build = get_build!(params[:build_id])
+
+          present build, with: ::API::V3::Entities::Build
+        end
+
+        desc 'Download the artifacts file from build' do
+          detail 'This feature was introduced in GitLab 8.5'
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        get ':id/builds/:build_id/artifacts' do
+          authorize_read_builds!
+
+          build = get_build!(params[:build_id])
+
+          present_artifacts!(build.artifacts_file)
+        end
+
+        desc 'Download the artifacts file from build' do
+          detail 'This feature was introduced in GitLab 8.10'
+        end
+        params do
+          requires :ref_name, type: String, desc: 'The ref from repository'
+          requires :job,      type: String, desc: 'The name for the build'
+        end
+        get ':id/builds/artifacts/:ref_name/download',
+          requirements: { ref_name: /.+/ } do
+          authorize_read_builds!
+
+          builds = user_project.latest_successful_builds_for(params[:ref_name])
+          latest_build = builds.find_by!(name: params[:job])
+
+          present_artifacts!(latest_build.artifacts_file)
+        end
+
+        # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+        #       is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+        #       `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+        desc 'Get a trace of a specific build of a project'
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        get ':id/builds/:build_id/trace' do
+          authorize_read_builds!
+
+          build = get_build!(params[:build_id])
+
+          header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+          content_type 'text/plain'
+          env['api.format'] = :binary
+
+          trace = build.trace
+          body trace
+        end
+
+        desc 'Cancel a specific build of a project' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        post ':id/builds/:build_id/cancel' do
+          authorize_update_builds!
+
+          build = get_build!(params[:build_id])
+
+          build.cancel
+
+          present build, with: ::API::V3::Entities::Build
+        end
+
+        desc 'Retry a specific build of a project' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        post ':id/builds/:build_id/retry' do
+          authorize_update_builds!
+
+          build = get_build!(params[:build_id])
+          return forbidden!('Build is not retryable') unless build.retryable?
+
+          build = Ci::Build.retry(build, current_user)
+
+          present build, with: ::API::V3::Entities::Build
+        end
+
+        desc 'Erase build (remove artifacts and build trace)' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        post ':id/builds/:build_id/erase' do
+          authorize_update_builds!
+
+          build = get_build!(params[:build_id])
+          return forbidden!('Build is not erasable!') unless build.erasable?
+
+          build.erase(erased_by: current_user)
+          present build, with: ::API::V3::Entities::Build
+        end
+
+        desc 'Keep the artifacts to prevent them from being deleted' do
+          success ::API::V3::Entities::Build
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a build'
+        end
+        post ':id/builds/:build_id/artifacts/keep' do
+          authorize_update_builds!
+
+          build = get_build!(params[:build_id])
+          return not_found!(build) unless build.artifacts?
+
+          build.keep_artifacts!
+
+          status 200
+          present build, with: ::API::V3::Entities::Build
+        end
+
+        desc 'Trigger a manual build' do
+          success ::API::V3::Entities::Build
+          detail 'This feature was added in GitLab 8.11'
+        end
+        params do
+          requires :build_id, type: Integer, desc: 'The ID of a Build'
+        end
+        post ":id/builds/:build_id/play" do
+          authorize_read_builds!
+
+          build = get_build!(params[:build_id])
+
+          bad_request!("Unplayable Job") unless build.playable?
+
+          build.play(current_user)
+
+          status 200
+          present build, with: ::API::V3::Entities::Build
+        end
+      end
+
+      helpers do
+        def get_build(id)
+          user_project.builds.find_by(id: id.to_i)
+        end
+
+        def get_build!(id)
+          get_build(id) || not_found!
+        end
+
+        def present_artifacts!(artifacts_file)
+          if !artifacts_file.file_storage?
+            redirect_to(build.artifacts_file.url)
+          elsif artifacts_file.exists?
+            present_file!(artifacts_file.path, artifacts_file.filename)
+          else
+            not_found!
+          end
+        end
+
+        def filter_builds(builds, scope)
+          return builds if scope.nil? || scope.empty?
+
+          available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+          unknown = scope - available_statuses
+          render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+          builds.where(status: available_statuses && scope)
+        end
+
+        def authorize_read_builds!
+          authorize! :read_build, user_project
+        end
+
+        def authorize_update_builds!
+          authorize! :update_build, user_project
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 477e22fd25e80421ff8eb6ae3bf3b224b10890d4..3414a2883e58f0b6eda34816c6c4c87f231e78d6 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -11,7 +11,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get a project repository commits' do
           success ::API::Entities::RepoCommit
         end
@@ -55,13 +55,6 @@ module API
           branch = attrs.delete(:branch_name)
           attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
 
-          attrs[:actions].map! do |action|
-            action[:action] = action[:action].to_sym
-            action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
-            action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
-            action
-          end
-
           result = ::Files::MultiService.new(user_project, current_user, attrs).execute
 
           if result[:status] == :success
@@ -137,9 +130,7 @@ module API
 
           commit_params = {
             commit: commit,
-            create_merge_request: false,
-            source_project: user_project,
-            source_branch: commit.cherry_pick_branch_name,
+            start_branch: params[:branch],
             target_branch: params[:branch]
           }
 
@@ -162,7 +153,7 @@ module API
           optional :path, type: String, desc: 'The file path'
           given :path do
             requires :line, type: Integer, desc: 'The line number'
-            requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+            requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
           end
         end
         post ':id/repository/commits/:sha/comments' do
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
index 5bbb167755cb7904620b938ee020f58acaa41c31..bbb174b6003c7fdad1bbe2f274524daec6405cad 100644
--- a/lib/api/v3/deploy_keys.rb
+++ b/lib/api/v3/deploy_keys.rb
@@ -13,7 +13,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of the project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         before { authorize_admin_project }
 
         %w(keys deploy_keys).each do |path|
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1d4972eda266996721df677f9e0013ecc8d1fc47
--- /dev/null
+++ b/lib/api/v3/deployments.rb
@@ -0,0 +1,43 @@
+module API
+  module V3
+    # Deployments RESTful API endpoints
+    class Deployments < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The project ID'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get all deployments of the project' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success ::API::V3::Deployments
+        end
+        params do
+          use :pagination
+        end
+        get ':id/deployments' do
+          authorize! :read_deployment, user_project
+
+          present paginate(user_project.deployments), with: ::API::V3::Deployments
+        end
+
+        desc 'Gets a specific deployment' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success ::API::V3::Deployments
+        end
+        params do
+          requires :deployment_id, type: Integer,  desc: 'The deployment ID'
+        end
+        get ':id/deployments/:deployment_id' do
+          authorize! :read_deployment, user_project
+
+          deployment = user_project.deployments.find(params[:deployment_id])
+
+          present deployment, with: ::API::V3::Deployments
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3cc0dc968a82bc46ec7bbe9055711a528f0728a6..832b4bdeb4fefce69ad954dc74bbbc2915a768f8 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -11,6 +11,243 @@ module API
           Gitlab::UrlBuilder.build(snippet)
         end
       end
+
+      class Note < Grape::Entity
+        expose :id
+        expose :note, as: :body
+        expose :attachment_identifier, as: :attachment
+        expose :author, using: ::API::Entities::UserBasic
+        expose :created_at, :updated_at
+        expose :system?, as: :system
+        expose :noteable_id, :noteable_type
+        # upvote? and downvote? are deprecated, always return false
+        expose(:upvote?)    { |note| false }
+        expose(:downvote?)  { |note| false }
+      end
+
+      class Event < Grape::Entity
+        expose :title, :project_id, :action_name
+        expose :target_id, :target_type, :author_id
+        expose :data, :target_title
+        expose :created_at
+        expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+        expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
+
+        expose :author_username do |event, options|
+          event.author&.username
+        end
+      end
+
+      class AwardEmoji < Grape::Entity
+        expose :id
+        expose :name
+        expose :user, using: ::API::Entities::UserBasic
+        expose :created_at, :updated_at
+        expose :awardable_id, :awardable_type
+      end
+
+      class Project < Grape::Entity
+        expose :id, :description, :default_branch, :tag_list
+        expose :public?, as: :public
+        expose :archived?, as: :archived
+        expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+        expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group }
+        expose :name, :name_with_namespace
+        expose :path, :path_with_namespace
+        expose :container_registry_enabled
+
+        # Expose old field names with the new permissions methods to keep API compatible
+        expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+        expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+        expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+        expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+        expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+
+        expose :created_at, :last_activity_at
+        expose :shared_runners_enabled
+        expose :lfs_enabled?, as: :lfs_enabled
+        expose :creator_id
+        expose :namespace, using: 'API::Entities::Namespace'
+        expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
+        expose :avatar_url
+        expose :star_count, :forks_count
+        expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
+        expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
+        expose :public_builds
+        expose :shared_with_groups do |project, options|
+          ::API::Entities::SharedGroup.represent(project.project_group_links.all, options)
+        end
+        expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds
+        expose :request_access_enabled
+        expose :only_allow_merge_if_all_discussions_are_resolved
+
+        expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics
+      end
+
+      class ProjectWithAccess < Project
+        expose :permissions do
+          expose :project_access, using: ::API::Entities::ProjectAccess do |project, options|
+            project.project_members.find_by(user_id: options[:current_user].id)
+          end
+
+          expose :group_access, using: ::API::Entities::GroupAccess do |project, options|
+            if project.group
+              project.group.group_members.find_by(user_id: options[:current_user].id)
+            end
+          end
+        end
+      end
+
+      class MergeRequest < Grape::Entity
+        expose :id, :iid
+        expose(:project_id) { |entity| entity.project.id }
+        expose :title, :description
+        expose :state, :created_at, :updated_at
+        expose :target_branch, :source_branch
+        expose :upvotes, :downvotes
+        expose :author, :assignee, using: ::API::Entities::UserBasic
+        expose :source_project_id, :target_project_id
+        expose :label_names, as: :labels
+        expose :work_in_progress?, as: :work_in_progress
+        expose :milestone, using: ::API::Entities::Milestone
+        expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds
+        expose :merge_status
+        expose :diff_head_sha, as: :sha
+        expose :merge_commit_sha
+        expose :subscribed do |merge_request, options|
+          merge_request.subscribed?(options[:current_user], options[:project])
+        end
+        expose :user_notes_count
+        expose :should_remove_source_branch?, as: :should_remove_source_branch
+        expose :force_remove_source_branch?, as: :force_remove_source_branch
+
+        expose :web_url do |merge_request, options|
+          Gitlab::UrlBuilder.build(merge_request)
+        end
+      end
+
+      class Group < Grape::Entity
+        expose :id, :name, :path, :description, :visibility_level
+        expose :lfs_enabled?, as: :lfs_enabled
+        expose :avatar_url
+        expose :web_url
+        expose :request_access_enabled
+        expose :full_name, :full_path
+        expose :parent_id
+
+        expose :statistics, if: :statistics do
+          with_options format_with: -> (value) { value.to_i } do
+            expose :storage_size
+            expose :repository_size
+            expose :lfs_objects_size
+            expose :build_artifacts_size
+          end
+        end
+      end
+
+      class GroupDetail < Group
+        expose :projects, using: Entities::Project
+        expose :shared_projects, using: Entities::Project
+      end
+
+      class ApplicationSetting < Grape::Entity
+        expose :id
+        expose :default_projects_limit
+        expose :signup_enabled
+        expose :signin_enabled
+        expose :gravatar_enabled
+        expose :sign_in_text
+        expose :after_sign_up_text
+        expose :created_at
+        expose :updated_at
+        expose :home_page_url
+        expose :default_branch_protection
+        expose :restricted_visibility_levels
+        expose :max_attachment_size
+        expose :session_expire_delay
+        expose :default_project_visibility
+        expose :default_snippet_visibility
+        expose :default_group_visibility
+        expose :domain_whitelist
+        expose :domain_blacklist_enabled
+        expose :domain_blacklist
+        expose :user_oauth_applications
+        expose :after_sign_out_path
+        expose :container_registry_token_expire_delay
+        expose :repository_storage
+        expose :repository_storages
+        expose :koding_enabled
+        expose :koding_url
+        expose :plantuml_enabled
+        expose :plantuml_url
+        expose :terminal_max_session_time
+      end
+
+      class Environment < ::API::Entities::EnvironmentBasic
+        expose :project, using: Entities::Project
+      end
+
+      class Trigger < Grape::Entity
+        expose :token, :created_at, :updated_at, :deleted_at, :last_used
+        expose :owner, using: ::API::Entities::UserBasic
+      end
+
+      class TriggerRequest < Grape::Entity
+        expose :id, :variables
+      end
+
+      class Build < Grape::Entity
+        expose :id, :status, :stage, :name, :ref, :tag, :coverage
+        expose :created_at, :started_at, :finished_at
+        expose :user, with: ::API::Entities::User
+        expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
+        expose :commit, with: ::API::Entities::RepoCommit
+        expose :runner, with: ::API::Entities::Runner
+        expose :pipeline, with: ::API::Entities::PipelineBasic
+      end
+
+      class BuildArtifactFile < Grape::Entity
+        expose :filename, :size
+      end
+
+      class Deployment < Grape::Entity
+        expose :id, :iid, :ref, :sha, :created_at
+        expose :user,        using: ::API::Entities::UserBasic
+        expose :environment, using: ::API::Entities::EnvironmentBasic
+        expose :deployable,  using: Entities::Build
+      end
+
+      class MergeRequestChanges < MergeRequest
+        expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+          compare.raw_diffs(all_diffs: true).to_a
+        end
+      end
+
+      class ProjectStatistics < Grape::Entity
+        expose :commit_count
+        expose :storage_size
+        expose :repository_size
+        expose :lfs_objects_size
+        expose :build_artifacts_size
+      end
+
+      class ProjectService < Grape::Entity
+        expose :id, :title, :created_at, :updated_at, :active
+        expose :push_events, :issues_events, :merge_requests_events
+        expose :tag_push_events, :note_events, :build_events, :pipeline_events
+        # Expose serialized properties
+        expose :properties do |service, options|
+          field_names = service.fields.
+            select { |field| options[:include_passwords] || field[:type] != 'password' }.
+            map { |field| field[:name] }
+          service.properties.slice(*field_names)
+        end
+      end
+
+      class ProjectHook < ::API::Entities::Hook
+        expose :project_id, :issues_events, :merge_requests_events
+        expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+      end
     end
   end
 end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6bb4e016a0193ad53629b814afb0b5f66777a9c4
--- /dev/null
+++ b/lib/api/v3/environments.rb
@@ -0,0 +1,87 @@
+module API
+  module V3
+    class Environments < Grape::API
+      include ::API::Helpers::CustomValidators
+      include PaginationParams
+
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The project ID'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get all environments of the project' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::Environment
+        end
+        params do
+          use :pagination
+        end
+        get ':id/environments' do
+          authorize! :read_environment, user_project
+
+          present paginate(user_project.environments), with: Entities::Environment
+        end
+
+        desc 'Creates a new environment' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::Environment
+        end
+        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
+
+          environment = user_project.environments.create(declared_params)
+
+          if environment.persisted?
+            present environment, with: Entities::Environment
+          else
+            render_validation_error!(environment)
+          end
+        end
+
+        desc 'Updates an existing environment' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::Environment
+        end
+        params do
+          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
+
+          environment = user_project.environments.find(params[:environment_id])
+
+          update_params = declared_params(include_missing: false).extract!(:name, :external_url)
+          if environment.update(update_params)
+            present environment, with: Entities::Environment
+          else
+            render_validation_error!(environment)
+          end
+        end
+
+        desc 'Deletes an existing environment' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::Environment
+        end
+        params do
+          requires :environment_id, type: Integer,  desc: 'The environment ID'
+        end
+        delete ':id/environments/:environment_id' do
+          authorize! :update_environment, user_project
+
+          environment = user_project.environments.find(params[:environment_id])
+
+          present environment.destroy, with: Entities::Environment
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
index 4f8d58d37c86ffb0ea75a57293ca4a7f8980f48d..13542b0c71c60ad83388ed69dc1d011c4eac2343 100644
--- a/lib/api/v3/files.rb
+++ b/lib/api/v3/files.rb
@@ -40,7 +40,7 @@ module API
       params do
         requires :id, type: String, desc: 'The project ID'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get a file from repository'
         params do
           requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c5b37622d79ab81026318ef88905d032429ba5e7
--- /dev/null
+++ b/lib/api/v3/groups.rb
@@ -0,0 +1,181 @@
+module API
+  module V3
+    class Groups < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      helpers do
+        params :optional_params do
+          optional :description, type: String, desc: 'The description of the group'
+          optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+          optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
+          optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+        end
+
+        params :statistics_params do
+          optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+        end
+
+        def present_groups(groups, options = {})
+          options = options.reverse_merge(
+            with: Entities::Group,
+            current_user: current_user,
+          )
+
+          groups = groups.with_statistics if options[:statistics]
+          present paginate(groups), options
+        end
+      end
+
+      resource :groups do
+        desc 'Get a groups list' do
+          success Entities::Group
+        end
+        params do
+          use :statistics_params
+          optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+          optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
+          optional :search, type: String, desc: 'Search for a specific group'
+          optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+          optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+          use :pagination
+        end
+        get do
+          groups = if current_user.admin
+                     Group.all
+                   elsif params[:all_available]
+                     GroupsFinder.new.execute(current_user)
+                   else
+                     current_user.groups
+                   end
+
+          groups = groups.search(params[:search]) if params[:search].present?
+          groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+          groups = groups.reorder(params[:order_by] => params[:sort])
+
+          present_groups groups, statistics: params[:statistics] && current_user.is_admin?
+        end
+
+        desc 'Get list of owned groups for authenticated user' do
+          success Entities::Group
+        end
+        params do
+          use :pagination
+          use :statistics_params
+        end
+        get '/owned' do
+          present_groups current_user.owned_groups, statistics: params[:statistics]
+        end
+
+        desc 'Create a group. Available only for users who can create groups.' do
+          success Entities::Group
+        end
+        params do
+          requires :name, type: String, desc: 'The name of the group'
+          requires :path, type: String, desc: 'The path of the group'
+          optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+          use :optional_params
+        end
+        post do
+          authorize! :create_group
+
+          group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
+
+          if group.persisted?
+            present group, with: Entities::Group, current_user: current_user
+          else
+            render_api_error!("Failed to save group #{group.errors.messages}", 400)
+          end
+        end
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a group'
+      end
+      resource :groups, requirements: { id: %r{[^/]+} } do
+        desc 'Update a group. Available only for users who can administrate groups.' do
+          success Entities::Group
+        end
+        params do
+          optional :name, type: String, desc: 'The name of the group'
+          optional :path, type: String, desc: 'The path of the group'
+          use :optional_params
+          at_least_one_of :name, :path, :description, :visibility_level,
+                          :lfs_enabled, :request_access_enabled
+        end
+        put ':id' do
+          group = find_group!(params[:id])
+          authorize! :admin_group, group
+
+          if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
+            present group, with: Entities::GroupDetail, current_user: current_user
+          else
+            render_validation_error!(group)
+          end
+        end
+
+        desc 'Get a single group, with containing projects.' do
+          success Entities::GroupDetail
+        end
+        get ":id" do
+          group = find_group!(params[:id])
+          present group, with: Entities::GroupDetail, current_user: current_user
+        end
+
+        desc 'Remove a group.'
+        delete ":id" do
+          group = find_group!(params[:id])
+          authorize! :admin_group, group
+          present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+        end
+
+        desc 'Get a list of projects in this group.' do
+          success Entities::Project
+        end
+        params do
+          optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+          optional :visibility, type: String, values: %w[public internal private],
+                                desc: 'Limit by visibility'
+          optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+          optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+                              default: 'created_at', desc: 'Return projects ordered by field'
+          optional :sort, type: String, values: %w[asc desc], default: 'desc',
+                          desc: 'Return projects sorted in ascending and descending order'
+          optional :simple, type: Boolean, default: false,
+                            desc: 'Return only the ID, URL, name, and path of each project'
+          optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
+          optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+
+          use :pagination
+        end
+        get ":id/projects" do
+          group = find_group!(params[:id])
+          projects = GroupProjectsFinder.new(group).execute(current_user)
+          projects = filter_projects(projects)
+          entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
+          present paginate(projects), with: entity, current_user: current_user
+        end
+
+        desc 'Transfer a project to the group namespace. Available only for admin.' do
+          success Entities::GroupDetail
+        end
+        params do
+          requires :project_id, type: String, desc: 'The ID or path of the project'
+        end
+        post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
+          authenticated_as_admin!
+          group = find_group!(params[:id])
+          project = find_project!(params[:project_id])
+          result = ::Projects::TransferService.new(project, current_user).execute(group)
+
+          if result
+            present group, with: Entities::GroupDetail, current_user: current_user
+          else
+            render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f234d4cdad51e9d2ab6de0e459f2511955f12db
--- /dev/null
+++ b/lib/api/v3/helpers.rb
@@ -0,0 +1,19 @@
+module API
+  module V3
+    module Helpers
+      def find_project_issue(id)
+        IssuesFinder.new(current_user, project_id: user_project.id).find(id)
+      end
+
+      def find_project_merge_request(id)
+        MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+      end
+
+      def find_merge_request_with_access(id, access_level = :read_merge_request)
+        merge_request = user_project.merge_requests.find(id)
+        authorize! access_level, merge_request
+        merge_request
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index d0af09f0e1ec7482ac0d3c21b46628119cd777f6..54c6a8060b825106a65b460741cc9e90a45c1396 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -68,7 +68,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a group'
       end
-      resource :groups do
+      resource :groups, requirements: { id: %r{[^/]+} } do
         desc 'Get a list of group issues' do
           success ::API::Entities::Issue
         end
@@ -89,7 +89,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         include TimeTrackingEndpoints
 
         desc 'Get a list of project issues' do
@@ -103,7 +103,7 @@ module API
           use :issues_params
         end
         get ":id/issues" do
-          project = find_project(params[:id])
+          project = find_project!(params[:id])
 
           issues = find_issues(project_id: project.id)
 
@@ -139,12 +139,7 @@ module API
           end
 
           issue_params = declared_params(include_missing: false)
-
-          if merge_request_iid = params[:merge_request_for_resolving_discussions]
-            issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
-              execute.
-              find_by(iid: merge_request_iid)
-          end
+          issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
 
           issue = ::Issues::CreateService.new(user_project,
                                               current_user,
@@ -226,6 +221,8 @@ module API
           not_found!('Issue') unless issue
 
           authorize!(:destroy_issue, issue)
+
+          status(200)
           issue.destroy
         end
       end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
index 5c3261311bf7bcfd7dd6a7bf63118440f28bd98b..bd5eb2175e83e4d2efe5506bc440ee6f5c64aba3 100644
--- a/lib/api/v3/labels.rb
+++ b/lib/api/v3/labels.rb
@@ -6,13 +6,28 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get all labels of the project' do
           success ::API::Entities::Label
         end
         get ':id/labels' do
           present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
         end
+
+        desc 'Delete an existing label' do
+          success ::API::Entities::Label
+        end
+        params do
+          requires :name, type: String, desc: 'The name of the label to be deleted'
+        end
+        delete ':id/labels' do
+          authorize! :admin_label, user_project
+
+          label = user_project.labels.find_by(title: params[:name])
+          not_found!('Label') unless label
+
+          present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project
+        end
       end
     end
   end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index 4e6cb2e3c5208b4146ec09687cc9ca9634713ecf..684860b553eb075a2c4e7fa0349ca8aab2d84b13 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -11,7 +11,7 @@ module API
         params do
           requires :id, type: String, desc: "The #{source_type} ID"
         end
-        resource source_type.pluralize do
+        resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
           desc 'Gets a list of group or project members viewable by the authenticated user.' do
             success ::API::Entities::Member
           end
@@ -86,13 +86,12 @@ module API
             optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
           end
           put ":id/members/:user_id" do
-            source = find_source(source_type, params[:id])
+            source = find_source(source_type, params.delete(:id))
             authorize_admin_source!(source_type, source)
 
-            member = source.members.find_by!(user_id: params[:user_id])
-            attrs = attributes_for_keys [:access_level, :expires_at]
+            member = source.members.find_by!(user_id: params.delete(:user_id))
 
-            if member.update_attributes(attrs)
+            if member.update_attributes(declared_params(include_missing: false))
               present member.user, with: ::API::Entities::Member, member: member
             else
               # This is to ensure back-compatibility but 400 behavior should be used
@@ -120,6 +119,7 @@ module API
             # This is to ensure back-compatibility but 204 behavior should be used
             # for all DELETE endpoints in 9.0!
             if member.nil?
+              status(200  )
               { message: "Access revoked", id: params[:user_id].to_i }
             else
               ::Members::DestroyService.new(source, current_user, declared_params).execute
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35f462e907b542d23a4c4a71bfef17aca08b486a
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,44 @@
+module API
+  module V3
+    # MergeRequestDiff API
+    class MergeRequestDiffs < Grape::API
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get a list of merge request diff versions' do
+          detail 'This feature was introduced in GitLab 8.12.'
+          success ::API::Entities::MergeRequestDiff
+        end
+
+        params do
+          requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        end
+
+        get ":id/merge_requests/:merge_request_id/versions" do
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+          present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff
+        end
+
+        desc 'Get a single merge request diff version' do
+          detail 'This feature was introduced in GitLab 8.12.'
+          success ::API::Entities::MergeRequestDiffFull
+        end
+
+        params do
+          requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+          requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+        end
+
+        get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+          merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+          present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 129f9d850e9693f25a83c65cb8c769974aea5ce8..3077240e6506eb33426094b91b29792a27616b69 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -10,7 +10,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         include TimeTrackingEndpoints
 
         helpers do
@@ -28,6 +28,14 @@ module API
             render_api_error!(errors, 400)
           end
 
+          def issue_entity(project)
+            if project.has_external_issue_tracker?
+              ::API::Entities::ExternalIssue
+            else
+              ::API::Entities::Issue
+            end
+          end
+
           params :optional_params do
             optional :description, type: String, desc: 'The description of the merge request'
             optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -39,7 +47,7 @@ module API
 
         desc 'List merge requests' do
           detail 'iid filter is deprecated have been removed on V4'
-          success ::API::Entities::MergeRequest
+          success ::API::V3::Entities::MergeRequest
         end
         params do
           optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -66,11 +74,11 @@ module API
             end
 
           merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
-          present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+          present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
         end
 
         desc 'Create a merge request' do
-          success ::API::Entities::MergeRequest
+          success ::API::V3::Entities::MergeRequest
         end
         params do
           requires :title, type: String, desc: 'The title of the merge request'
@@ -89,7 +97,7 @@ module API
           merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
 
           if merge_request.valid?
-            present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+            present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
           else
             handle_merge_request_errors! merge_request.errors
           end
@@ -103,6 +111,8 @@ module API
           merge_request = find_project_merge_request(params[:merge_request_id])
 
           authorize!(:destroy_merge_request, merge_request)
+
+          status(200)
           merge_request.destroy
         end
 
@@ -114,12 +124,12 @@ module API
             if status == :deprecated
               detail DEPRECATION_MESSAGE
             end
-            success ::API::Entities::MergeRequest
+            success ::API::V3::Entities::MergeRequest
           end
           get path do
             merge_request = find_merge_request_with_access(params[:merge_request_id])
 
-            present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+            present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
           end
 
           desc 'Get the commits of a merge request' do
@@ -141,7 +151,7 @@ module API
           end
 
           desc 'Update a merge request' do
-            success ::API::Entities::MergeRequest
+            success ::API::V3::Entities::MergeRequest
           end
           params do
             optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
@@ -162,21 +172,21 @@ module API
             merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
 
             if merge_request.valid?
-              present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+              present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
             else
               handle_merge_request_errors! merge_request.errors
             end
           end
 
           desc 'Merge a merge request' do
-            success ::API::Entities::MergeRequest
+            success ::API::V3::Entities::MergeRequest
           end
           params do
             optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
             optional :should_remove_source_branch, type: Boolean,
                                                    desc: 'When true, the source branch will be deleted if possible'
             optional :merge_when_build_succeeds, type: Boolean,
-                                                 desc: 'When true, this merge request will be merged when the pipeline succeeds'
+                                                 desc: 'When true, this merge request will be merged when the build succeeds'
             optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
           end
           put "#{path}/merge" do
@@ -209,16 +219,16 @@ module API
                 .execute(merge_request)
             end
 
-            present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+            present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
           end
 
-          desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
-            success ::API::Entities::MergeRequest
+          desc 'Cancel merge if "Merge When Build succeeds" is enabled' do
+            success ::API::V3::Entities::MergeRequest
           end
           post "#{path}/cancel_merge_when_build_succeeds" do
             merge_request = find_project_merge_request(params[:merge_request_id])
 
-            unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+            unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
 
             ::MergeRequest::MergeWhenPipelineSucceedsService
               .new(merge_request.target_project, current_user)
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be90cec4afcbc866c700cc24dca4fb518fdadcc6
--- /dev/null
+++ b/lib/api/v3/milestones.rb
@@ -0,0 +1,64 @@
+module API
+  module V3
+    class Milestones < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      helpers do
+        def filter_milestones_state(milestones, state)
+          case state
+          when 'active' then milestones.active
+          when 'closed' then milestones.closed
+          else milestones
+          end
+        end
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get a list of project milestones' do
+          success ::API::Entities::Milestone
+        end
+        params do
+          optional :state, type: String, values: %w[active closed all], default: 'all',
+                           desc: 'Return "active", "closed", or "all" milestones'
+          optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+          use :pagination
+        end
+        get ":id/milestones" do
+          authorize! :read_milestone, user_project
+
+          milestones = user_project.milestones
+          milestones = filter_milestones_state(milestones, params[:state])
+          milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+
+          present paginate(milestones), with: ::API::Entities::Milestone
+        end
+
+        desc 'Get all issues for a single project milestone' do
+          success ::API::Entities::Issue
+        end
+        params do
+          requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+          use :pagination
+        end
+        get ':id/milestones/:milestone_id/issues' do
+          authorize! :read_milestone, user_project
+
+          milestone = user_project.milestones.find(params[:milestone_id])
+
+          finder_params = {
+            project_id: user_project.id,
+            milestone_title: milestone.title
+          }
+
+          issues = IssuesFinder.new(current_user, finder_params).execute
+          present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f8e0eff4ffe2b2bf44e27b659183646abda6036
--- /dev/null
+++ b/lib/api/v3/notes.rb
@@ -0,0 +1,148 @@
+module API
+  module V3
+    class Notes < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        NOTEABLE_TYPES.each do |noteable_type|
+          noteables_str = noteable_type.to_s.underscore.pluralize
+
+          desc 'Get a list of project +noteable+ notes' do
+            success ::API::V3::Entities::Note
+          end
+          params do
+            requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+            use :pagination
+          end
+          get ":id/#{noteables_str}/:noteable_id/notes" do
+            noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+            if can?(current_user, noteable_read_ability_name(noteable), noteable)
+              # We exclude notes that are cross-references and that cannot be viewed
+              # by the current user. By doing this exclusion at this level and not
+              # at the DB query level (which we cannot in that case), the current
+              # page can have less elements than :per_page even if
+              # there's more than one page.
+              notes =
+                # paginate() only works with a relation. This could lead to a
+                # mismatch between the pagination headers info and the actual notes
+                # array returned, but this is really a edge-case.
+                paginate(noteable.notes).
+                reject { |n| n.cross_reference_not_visible_for?(current_user) }
+              present notes, with: ::API::V3::Entities::Note
+            else
+              not_found!("Notes")
+            end
+          end
+
+          desc 'Get a single +noteable+ note' do
+            success ::API::V3::Entities::Note
+          end
+          params do
+            requires :note_id, type: Integer, desc: 'The ID of a note'
+            requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+          end
+          get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+            noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+            note = noteable.notes.find(params[:note_id])
+            can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+
+            if can_read_note
+              present note, with: ::API::V3::Entities::Note
+            else
+              not_found!("Note")
+            end
+          end
+
+          desc 'Create a new +noteable+ note' do
+            success ::API::V3::Entities::Note
+          end
+          params do
+            requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+            requires :body, type: String, desc: 'The content of a note'
+            optional :created_at, type: String, desc: 'The creation date of the note'
+          end
+          post ":id/#{noteables_str}/:noteable_id/notes" do
+            opts = {
+              note: params[:body],
+              noteable_type: noteables_str.classify,
+              noteable_id: params[:noteable_id]
+            }
+
+            noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+            if can?(current_user, noteable_read_ability_name(noteable), noteable)
+              if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+                opts[:created_at] = params[:created_at]
+              end
+
+              note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+              if note.valid?
+                present note, with: ::API::V3::Entities.const_get(note.class.name)
+              else
+                not_found!("Note #{note.errors.messages}")
+              end
+            else
+              not_found!("Note")
+            end
+          end
+
+          desc 'Update an existing +noteable+ note' do
+            success ::API::V3::Entities::Note
+          end
+          params do
+            requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+            requires :note_id, type: Integer, desc: 'The ID of a note'
+            requires :body, type: String, desc: 'The content of a note'
+          end
+          put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+            note = user_project.notes.find(params[:note_id])
+
+            authorize! :admin_note, note
+
+            opts = {
+              note: params[:body]
+            }
+
+            note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+
+            if note.valid?
+              present note, with: ::API::V3::Entities::Note
+            else
+              render_api_error!("Failed to save note #{note.errors.messages}", 400)
+            end
+          end
+
+          desc 'Delete a +noteable+ note' do
+            success ::API::V3::Entities::Note
+          end
+          params do
+            requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+            requires :note_id, type: Integer, desc: 'The ID of a note'
+          end
+          delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+            note = user_project.notes.find(params[:note_id])
+            authorize! :admin_note, note
+
+            ::Notes::DestroyService.new(user_project, current_user).execute(note)
+
+            present note, with: ::API::V3::Entities::Note
+          end
+        end
+      end
+
+      helpers do
+        def noteable_read_ability_name(noteable)
+          "read_#{noteable.class.to_s.underscore}".to_sym
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..828272492445632896f6c87de84f2f32c05af811
--- /dev/null
+++ b/lib/api/v3/pipelines.rb
@@ -0,0 +1,36 @@
+module API
+  module V3
+    class Pipelines < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The project ID'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get all Pipelines of the project' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success ::API::Entities::Pipeline
+        end
+        params do
+          use :pagination
+          optional :scope,    type: String, values: %w(running branches tags),
+                              desc: 'Either running, branches, or tags'
+        end
+        get ':id/pipelines' do
+          authorize! :read_pipeline, user_project
+
+          pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+          present paginate(pipelines), with: ::API::Entities::Pipeline
+        end
+      end
+
+      helpers do
+        def pipeline
+          @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94614bfc8b64f3a5b35929360ef103659923af1d
--- /dev/null
+++ b/lib/api/v3/project_hooks.rb
@@ -0,0 +1,106 @@
+module API
+  module V3
+    class ProjectHooks < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+      before { authorize_admin_project }
+
+      helpers do
+        params :project_hook_properties do
+          requires :url, type: String, desc: "The URL to send the request to"
+          optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+          optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+          optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
+          optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+          optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
+          optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+          optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
+          optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
+          optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+          optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
+        end
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Get project hooks' do
+          success ::API::V3::Entities::ProjectHook
+        end
+        params do
+          use :pagination
+        end
+        get ":id/hooks" do
+          hooks = paginate user_project.hooks
+
+          present hooks, with: ::API::V3::Entities::ProjectHook
+        end
+
+        desc 'Get a project hook' do
+          success ::API::V3::Entities::ProjectHook
+        end
+        params do
+          requires :hook_id, type: Integer, desc: 'The ID of a project hook'
+        end
+        get ":id/hooks/:hook_id" do
+          hook = user_project.hooks.find(params[:hook_id])
+          present hook, with: ::API::V3::Entities::ProjectHook
+        end
+
+        desc 'Add hook to project' do
+          success ::API::V3::Entities::ProjectHook
+        end
+        params do
+          use :project_hook_properties
+        end
+        post ":id/hooks" do
+          hook = user_project.hooks.new(declared_params(include_missing: false))
+
+          if hook.save
+            present hook, with: ::API::V3::Entities::ProjectHook
+          else
+            error!("Invalid url given", 422) if hook.errors[:url].present?
+
+            not_found!("Project hook #{hook.errors.messages}")
+          end
+        end
+
+        desc 'Update an existing project hook' do
+          success ::API::V3::Entities::ProjectHook
+        end
+        params do
+          requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+          use :project_hook_properties
+        end
+        put ":id/hooks/:hook_id" do
+          hook = user_project.hooks.find(params.delete(:hook_id))
+
+          if hook.update_attributes(declared_params(include_missing: false))
+            present hook, with: ::API::V3::Entities::ProjectHook
+          else
+            error!("Invalid url given", 422) if hook.errors[:url].present?
+
+            not_found!("Project hook #{hook.errors.messages}")
+          end
+        end
+
+        desc 'Deletes project hook' do
+          success ::API::V3::Entities::ProjectHook
+        end
+        params do
+          requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
+        end
+        delete ":id/hooks/:hook_id" do
+          begin
+            present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook
+          rescue
+            # ProjectHook can raise Error if hook_id not found
+            not_found!("Error deleting hook #{params[:hook_id]}")
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index e03e941d30b0ae498ac7fbe303882c0d43a75cc4..fc065a22d74f53814384b33addea614bc25a1b82 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -8,7 +8,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         helpers do
           def handle_project_member_errors(errors)
             if errors[:project_access].any?
@@ -121,6 +121,8 @@ module API
 
           authorize! :admin_project_snippet, snippet
           snippet.destroy
+
+          status(200)
         end
 
         desc 'Get a raw project snippet'
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 6796da83f07951839c22ad4c0fc7e00da06dbe70..b753dbab381471cf747acdc4b8938b72308fef34 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -5,6 +5,10 @@ module API
 
       before { authenticate_non_get! }
 
+      after_validation do
+        set_only_allow_merge_if_pipeline_succeeds!
+      end
+
       helpers do
         params :optional_params do
           optional :description, type: String, desc: 'The description of the project'
@@ -20,10 +24,12 @@ module API
           optional :visibility_level, type: Integer, values: [
             Gitlab::VisibilityLevel::PRIVATE,
             Gitlab::VisibilityLevel::INTERNAL,
-            Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+            Gitlab::VisibilityLevel::PUBLIC
+          ], desc: 'Create a public project. The same as visibility_level = 20.'
           optional :public_builds, type: Boolean, desc: 'Perform public builds'
           optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
           optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+          optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
           optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
         end
 
@@ -36,6 +42,12 @@ module API
           end
           attrs
         end
+
+        def set_only_allow_merge_if_pipeline_succeeds!
+          if params.has_key?(:only_allow_merge_if_build_succeeds)
+            params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
+          end
+        end
       end
 
       resource :projects do
@@ -74,7 +86,7 @@ module API
 
           def present_projects(projects, options = {})
             options = options.reverse_merge(
-              with: ::API::Entities::Project,
+              with: ::API::V3::Entities::Project,
               current_user: current_user,
               simple: params[:simple],
             )
@@ -94,7 +106,7 @@ module API
           use :collection_params
         end
         get '/visible' do
-          entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+          entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
           present_projects ProjectsFinder.new.execute(current_user), with: entity
         end
 
@@ -108,7 +120,7 @@ module API
           authenticate!
 
           present_projects current_user.authorized_projects,
-            with: ::API::Entities::ProjectWithAccess
+            with: ::API::V3::Entities::ProjectWithAccess
         end
 
         desc 'Get an owned projects list for authenticated user' do
@@ -122,7 +134,7 @@ module API
           authenticate!
 
           present_projects current_user.owned_projects,
-            with: ::API::Entities::ProjectWithAccess,
+            with: ::API::V3::Entities::ProjectWithAccess,
             statistics: params[:statistics]
         end
 
@@ -148,11 +160,11 @@ module API
         get '/all' do
           authenticated_as_admin!
 
-          present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics]
+          present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
         end
 
         desc 'Search for projects the current user has access to' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         params do
           requires :query, type: String, desc: 'The project name to be searched'
@@ -164,15 +176,16 @@ module API
           projects = search_service.objects('projects', params[:page])
           projects = projects.reorder(params[:order_by] => params[:sort])
 
-          present paginate(projects), with: ::API::Entities::Project
+          present paginate(projects), with: ::API::V3::Entities::Project
         end
 
         desc 'Create new project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         params do
-          requires :name, type: String, desc: 'The name of the project'
+          optional :name, type: String, desc: 'The name of the project'
           optional :path, type: String, desc: 'The path of the repository'
+          at_least_one_of :name, :path
           use :optional_params
           use :create_params
         end
@@ -181,7 +194,7 @@ module API
           project = ::Projects::CreateService.new(current_user, attrs).execute
 
           if project.saved?
-            present project, with: ::API::Entities::Project,
+            present project, with: ::API::V3::Entities::Project,
                              user_can_admin_project: can?(current_user, :admin_project, project)
           else
             if project.errors[:limit_reached].present?
@@ -192,7 +205,7 @@ module API
         end
 
         desc 'Create new project for a specified user. Only available to admin users.' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         params do
           requires :name, type: String, desc: 'The name of the project'
@@ -210,7 +223,7 @@ module API
           project = ::Projects::CreateService.new(user, attrs).execute
 
           if project.saved?
-            present project, with: ::API::Entities::Project,
+            present project, with: ::API::V3::Entities::Project,
                              user_can_admin_project: can?(current_user, :admin_project, project)
           else
             render_validation_error!(project)
@@ -221,28 +234,28 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects, requirements: { id: /[^\/]+/ } do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get a single project' do
-          success ::API::Entities::ProjectWithAccess
+          success ::API::V3::Entities::ProjectWithAccess
         end
         get ":id" do
-          entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+          entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
           present user_project, with: entity, current_user: current_user,
                                 user_can_admin_project: can?(current_user, :admin_project, user_project)
         end
 
         desc 'Get events for a single project' do
-          success ::API::Entities::Event
+          success ::API::V3::Entities::Event
         end
         params do
           use :pagination
         end
         get ":id/events" do
-          present paginate(user_project.events.recent), with: ::API::Entities::Event
+          present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
         end
 
         desc 'Fork new project for the current user or provided namespace.' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         params do
           optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
@@ -268,13 +281,13 @@ module API
           if forked_project.errors.any?
             conflict!(forked_project.errors.messages)
           else
-            present forked_project, with: ::API::Entities::Project,
+            present forked_project, with: ::API::V3::Entities::Project,
                                     user_can_admin_project: can?(current_user, :admin_project, forked_project)
           end
         end
 
         desc 'Update an existing project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         params do
           optional :name, type: String, desc: 'The name of the project'
@@ -298,7 +311,7 @@ module API
           result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
 
           if result[:status] == :success
-            present user_project, with: ::API::Entities::Project,
+            present user_project, with: ::API::V3::Entities::Project,
                                   user_can_admin_project: can?(current_user, :admin_project, user_project)
           else
             render_validation_error!(user_project)
@@ -306,29 +319,29 @@ module API
         end
 
         desc 'Archive a project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         post ':id/archive' do
           authorize!(:archive_project, user_project)
 
           user_project.archive!
 
-          present user_project, with: ::API::Entities::Project
+          present user_project, with: ::API::V3::Entities::Project
         end
 
         desc 'Unarchive a project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         post ':id/unarchive' do
           authorize!(:archive_project, user_project)
 
           user_project.unarchive!
 
-          present user_project, with: ::API::Entities::Project
+          present user_project, with: ::API::V3::Entities::Project
         end
 
         desc 'Star a project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         post ':id/star' do
           if current_user.starred?(user_project)
@@ -337,19 +350,19 @@ module API
             current_user.toggle_star(user_project)
             user_project.reload
 
-            present user_project, with: ::API::Entities::Project
+            present user_project, with: ::API::V3::Entities::Project
           end
         end
 
         desc 'Unstar a project' do
-          success ::API::Entities::Project
+          success ::API::V3::Entities::Project
         end
         delete ':id/star' do
           if current_user.starred?(user_project)
             current_user.toggle_star(user_project)
             user_project.reload
 
-            present user_project, with: ::API::Entities::Project
+            present user_project, with: ::API::V3::Entities::Project
           else
             not_modified!
           end
@@ -358,6 +371,8 @@ module API
         desc 'Remove a project'
         delete ":id" do
           authorize! :remove_project, user_project
+
+          status(200)
           ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
         end
 
@@ -383,6 +398,7 @@ module API
           authorize! :remove_fork_project, user_project
 
           if user_project.forked?
+            status(200)
             user_project.forked_project_link.destroy
           else
             not_modified!
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 3549ea225eff085e08c0babfe78f21e776cd0561..e4d14bc81686c6cf6d064bab766ac80a4e2407ea 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -8,7 +8,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         helpers do
           def handle_project_member_errors(errors)
             if errors[:project_access].any?
@@ -38,6 +38,60 @@ module API
           present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
         end
 
+        desc 'Get a raw file contents'
+        params do
+          requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+          requires :filepath, type: String, desc: 'The path to the file to display'
+        end
+        get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
+          repo = user_project.repository
+          commit = repo.commit(params[:sha])
+          not_found! "Commit" unless commit
+          blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
+          not_found! "File" unless blob
+          send_git_blob repo, blob
+        end
+
+        desc 'Get a raw blob contents by blob sha'
+        params do
+          requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+        end
+        get ':id/repository/raw_blobs/:sha' do
+          repo = user_project.repository
+          begin
+            blob = Gitlab::Git::Blob.raw(repo, params[:sha])
+          rescue
+            not_found! 'Blob'
+          end
+          not_found! 'Blob' unless blob
+          send_git_blob repo, blob
+        end
+
+        desc 'Get an archive of the repository'
+        params do
+          optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
+          optional :format, type: String, desc: 'The archive format'
+        end
+        get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
+          begin
+            send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
+          rescue
+            not_found!('File')
+          end
+        end
+
+        desc 'Compare two branches, tags, or commits' do
+          success ::API::Entities::Compare
+        end
+        params do
+          requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
+          requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+        end
+        get ':id/repository/compare' do
+          compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
+          present compare, with: ::API::Entities::Compare
+        end
+
         desc 'Get repository contributors' do
           success ::API::Entities::Contributor
         end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1934d6e578cb063013473babaf206f5b0b04cce7
--- /dev/null
+++ b/lib/api/v3/runners.rb
@@ -0,0 +1,65 @@
+module API
+  module V3
+    class Runners < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      resource :runners do
+        desc 'Remove a runner' do
+          success ::API::Entities::Runner
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the runner'
+        end
+        delete ':id' do
+          runner = Ci::Runner.find(params[:id])
+          not_found!('Runner') unless runner
+
+          authenticate_delete_runner!(runner)
+
+          status(200)
+          runner.destroy
+        end
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        before { authorize_admin_project }
+
+        desc "Disable project's runner" do
+          success ::API::Entities::Runner
+        end
+        params do
+          requires :runner_id, type: Integer, desc: 'The ID of the runner'
+        end
+        delete ':id/runners/:runner_id' do
+          runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
+          not_found!('Runner') unless runner_project
+
+          runner = runner_project.runner
+          forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
+
+          runner_project.destroy
+
+          present runner, with: ::API::Entities::Runner
+        end
+      end
+
+      helpers do
+        def authenticate_delete_runner!(runner)
+          return if current_user.is_admin?
+          forbidden!("Runner is shared") if runner.is_shared?
+          forbidden!("Runner associated with more than one project") if runner.projects.count > 1
+          forbidden!("No access granted") unless user_can_access_runner?(runner)
+        end
+
+        def user_can_access_runner?(runner)
+          current_user.ci_authorized_runners.exists?(runner.id)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3bacaeee0323354b160a127759beb863e8742e37
--- /dev/null
+++ b/lib/api/v3/services.rb
@@ -0,0 +1,644 @@
+module API
+  module V3
+    class Services < Grape::API
+      services = {
+        'asana' => [
+          {
+            required: true,
+            name: :api_key,
+            type: String,
+            desc: 'User API token'
+          },
+          {
+            required: false,
+            name: :restrict_to_branch,
+            type: String,
+            desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+          }
+        ],
+        'assembla' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'The authentication token'
+          },
+          {
+            required: false,
+            name: :subdomain,
+            type: String,
+            desc: 'Subdomain setting'
+          }
+        ],
+        'bamboo' => [
+          {
+            required: true,
+            name: :bamboo_url,
+            type: String,
+            desc: 'Bamboo root URL like https://bamboo.example.com'
+          },
+          {
+            required: true,
+            name: :build_key,
+            type: String,
+            desc: 'Bamboo build plan key like'
+          },
+          {
+            required: true,
+            name: :username,
+            type: String,
+            desc: 'A user with API access, if applicable'
+          },
+          {
+            required: true,
+            name: :password,
+            type: String,
+            desc: 'Passord of the user'
+          }
+        ],
+        'bugzilla' => [
+          {
+            required: true,
+            name: :new_issue_url,
+            type: String,
+            desc: 'New issue URL'
+          },
+          {
+            required: true,
+            name: :issues_url,
+            type: String,
+            desc: 'Issues URL'
+          },
+          {
+            required: true,
+            name: :project_url,
+            type: String,
+            desc: 'Project URL'
+          },
+          {
+            required: false,
+            name: :description,
+            type: String,
+            desc: 'Description'
+          },
+          {
+            required: false,
+            name: :title,
+            type: String,
+            desc: 'Title'
+          }
+        ],
+        'buildkite' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'Buildkite project GitLab token'
+          },
+          {
+            required: true,
+            name: :project_url,
+            type: String,
+            desc: 'The buildkite project URL'
+          },
+          {
+            required: false,
+            name: :enable_ssl_verification,
+            type: Boolean,
+            desc: 'Enable SSL verification for communication'
+          }
+        ],
+        'builds-email' => [
+          {
+            required: true,
+            name: :recipients,
+            type: String,
+            desc: 'Comma-separated list of recipient email addresses'
+          },
+          {
+            required: false,
+            name: :add_pusher,
+            type: Boolean,
+            desc: 'Add pusher to recipients list'
+          },
+          {
+            required: false,
+            name: :notify_only_broken_builds,
+            type: Boolean,
+            desc: 'Notify only broken builds'
+          }
+        ],
+        'campfire' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'Campfire token'
+          },
+          {
+            required: false,
+            name: :subdomain,
+            type: String,
+            desc: 'Campfire subdomain'
+          },
+          {
+            required: false,
+            name: :room,
+            type: String,
+            desc: 'Campfire room'
+          }
+        ],
+        'custom-issue-tracker' => [
+          {
+            required: true,
+            name: :new_issue_url,
+            type: String,
+            desc: 'New issue URL'
+          },
+          {
+            required: true,
+            name: :issues_url,
+            type: String,
+            desc: 'Issues URL'
+          },
+          {
+            required: true,
+            name: :project_url,
+            type: String,
+            desc: 'Project URL'
+          },
+          {
+            required: false,
+            name: :description,
+            type: String,
+            desc: 'Description'
+          },
+          {
+            required: false,
+            name: :title,
+            type: String,
+            desc: 'Title'
+          }
+        ],
+        'drone-ci' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'Drone CI token'
+          },
+          {
+            required: true,
+            name: :drone_url,
+            type: String,
+            desc: 'Drone CI URL'
+          },
+          {
+            required: false,
+            name: :enable_ssl_verification,
+            type: Boolean,
+            desc: 'Enable SSL verification for communication'
+          }
+        ],
+        'emails-on-push' => [
+          {
+            required: true,
+            name: :recipients,
+            type: String,
+            desc: 'Comma-separated list of recipient email addresses'
+          },
+          {
+            required: false,
+            name: :disable_diffs,
+            type: Boolean,
+            desc: 'Disable code diffs'
+          },
+          {
+            required: false,
+            name: :send_from_committer_email,
+            type: Boolean,
+            desc: 'Send from committer'
+          }
+        ],
+        'external-wiki' => [
+          {
+            required: true,
+            name: :external_wiki_url,
+            type: String,
+            desc: 'The URL of the external Wiki'
+          }
+        ],
+        'flowdock' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'Flowdock token'
+          }
+        ],
+        'gemnasium' => [
+          {
+            required: true,
+            name: :api_key,
+            type: String,
+            desc: 'Your personal API key on gemnasium.com'
+          },
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: "The project's slug on gemnasium.com"
+          }
+        ],
+        'hipchat' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'The room token'
+          },
+          {
+            required: false,
+            name: :room,
+            type: String,
+            desc: 'The room name or ID'
+          },
+          {
+            required: false,
+            name: :color,
+            type: String,
+            desc: 'The room color'
+          },
+          {
+            required: false,
+            name: :notify,
+            type: Boolean,
+            desc: 'Enable notifications'
+          },
+          {
+            required: false,
+            name: :api_version,
+            type: String,
+            desc: 'Leave blank for default (v2)'
+          },
+          {
+            required: false,
+            name: :server,
+            type: String,
+            desc: 'Leave blank for default. https://hipchat.example.com'
+          }
+        ],
+        'irker' => [
+          {
+            required: true,
+            name: :recipients,
+            type: String,
+            desc: 'Recipients/channels separated by whitespaces'
+          },
+          {
+            required: false,
+            name: :default_irc_uri,
+            type: String,
+            desc: 'Default: irc://irc.network.net:6697'
+          },
+          {
+            required: false,
+            name: :server_host,
+            type: String,
+            desc: 'Server host. Default localhost'
+          },
+          {
+            required: false,
+            name: :server_port,
+            type: Integer,
+            desc: 'Server port. Default 6659'
+          },
+          {
+            required: false,
+            name: :colorize_messages,
+            type: Boolean,
+            desc: 'Colorize messages'
+          }
+        ],
+        'jira' => [
+          {
+            required: true,
+            name: :url,
+            type: String,
+            desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+          },
+          {
+            required: true,
+            name: :project_key,
+            type: String,
+            desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+          },
+          {
+            required: false,
+            name: :username,
+            type: String,
+            desc: 'The username of the user created to be used with GitLab/JIRA'
+          },
+          {
+            required: false,
+            name: :password,
+            type: String,
+            desc: 'The password of the user created to be used with GitLab/JIRA'
+          },
+          {
+            required: false,
+            name: :jira_issue_transition_id,
+            type: Integer,
+            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,
+            name: :token,
+            type: String,
+            desc: 'The Mattermost token'
+          }
+        ],
+        'slack-slash-commands' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'The Slack token'
+          }
+        ],
+        'pipelines-email' => [
+          {
+            required: true,
+            name: :recipients,
+            type: String,
+            desc: 'Comma-separated list of recipient email addresses'
+          },
+          {
+            required: false,
+            name: :notify_only_broken_builds,
+            type: Boolean,
+            desc: 'Notify only broken builds'
+          }
+        ],
+        'pivotaltracker' => [
+          {
+            required: true,
+            name: :token,
+            type: String,
+            desc: 'The Pivotaltracker token'
+          },
+          {
+            required: false,
+            name: :restrict_to_branch,
+            type: String,
+            desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+          }
+        ],
+        'pushover' => [
+          {
+            required: true,
+            name: :api_key,
+            type: String,
+            desc: 'The application key'
+          },
+          {
+            required: true,
+            name: :user_key,
+            type: String,
+            desc: 'The user key'
+          },
+          {
+            required: true,
+            name: :priority,
+            type: String,
+            desc: 'The priority'
+          },
+          {
+            required: true,
+            name: :device,
+            type: String,
+            desc: 'Leave blank for all active devices'
+          },
+          {
+            required: true,
+            name: :sound,
+            type: String,
+            desc: 'The sound of the notification'
+          }
+        ],
+        'redmine' => [
+          {
+            required: true,
+            name: :new_issue_url,
+            type: String,
+            desc: 'The new issue URL'
+          },
+          {
+            required: true,
+            name: :project_url,
+            type: String,
+            desc: 'The project URL'
+          },
+          {
+            required: true,
+            name: :issues_url,
+            type: String,
+            desc: 'The issues URL'
+          },
+          {
+            required: false,
+            name: :description,
+            type: String,
+            desc: 'The description of the tracker'
+          }
+        ],
+        'slack' => [
+          {
+            required: true,
+            name: :webhook,
+            type: String,
+            desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+          },
+          {
+            required: false,
+            name: :new_issue_url,
+            type: String,
+            desc: 'The user name'
+          },
+          {
+            required: false,
+            name: :channel,
+            type: String,
+            desc: 'The channel name'
+          }
+        ],
+        'mattermost' => [
+          {
+            required: true,
+            name: :webhook,
+            type: String,
+            desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+          }
+        ],
+        'teamcity' => [
+          {
+            required: true,
+            name: :teamcity_url,
+            type: String,
+            desc: 'TeamCity root URL like https://teamcity.example.com'
+          },
+          {
+            required: true,
+            name: :build_type,
+            type: String,
+            desc: 'Build configuration ID'
+          },
+          {
+            required: true,
+            name: :username,
+            type: String,
+            desc: 'A user with permissions to trigger a manual build'
+          },
+          {
+            required: true,
+            name: :password,
+            type: String,
+            desc: 'The password of the user'
+          }
+        ]
+      }
+
+      trigger_services = {
+        'mattermost-slash-commands' => [
+          {
+            name: :token,
+            type: String,
+            desc: 'The Mattermost token'
+          }
+        ],
+        'slack-slash-commands' => [
+          {
+            name: :token,
+            type: String,
+            desc: 'The Slack token'
+          }
+        ]
+      }.freeze
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        before { authenticate! }
+        before { authorize_admin_project }
+
+        helpers do
+          def service_attributes(service)
+            service.fields.inject([]) do |arr, hash|
+              arr << hash[:name].to_sym
+            end
+          end
+        end
+
+        desc "Delete a service for project"
+        params do
+          requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+        end
+        delete ":id/services/:service_slug" do
+          service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+
+          attrs = service_attributes(service).inject({}) do |hash, key|
+            hash.merge!(key => nil)
+          end
+
+          if service.update_attributes(attrs.merge(active: false))
+            status(200)
+            true
+          else
+            render_api_error!('400 Bad Request', 400)
+          end
+        end
+
+        desc 'Get the service settings for project' do
+          success Entities::ProjectService
+        end
+        params do
+          requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+        end
+        get ":id/services/:service_slug" do
+          service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+          present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+        end
+      end
+
+      trigger_services.each do |service_slug, settings|
+        helpers do
+          def chat_command_service(project, service_slug, params)
+            project.services.active.where(template: false).find do |service|
+              service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+            end
+          end
+        end
+
+        params do
+          requires :id, type: String, desc: 'The ID of a project'
+        end
+        resource :projects, requirements: { id: %r{[^/]+} } do
+          desc "Trigger a slash command for #{service_slug}" do
+            detail 'Added in GitLab 8.13'
+          end
+          params do
+            settings.each do |setting|
+              requires setting[:name], type: setting[:type], desc: setting[:desc]
+            end
+          end
+          post ":id/services/#{service_slug.underscore}/trigger" do
+            project = find_project(params[:id])
+
+            # This is not accurate, but done to prevent leakage of the project names
+            not_found!('Service') unless project
+
+            service = chat_command_service(project, service_slug, params)
+            result = service.try(:trigger, params)
+
+            if result
+              status result[:status] || 200
+              present result
+            else
+              not_found!('Service')
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..748d6b97d4f949f3c930028d56c37940db179a73
--- /dev/null
+++ b/lib/api/v3/settings.rb
@@ -0,0 +1,137 @@
+module API
+  module V3
+    class Settings < Grape::API
+      before { authenticated_as_admin! }
+
+      helpers do
+        def current_settings
+          @current_setting ||=
+            (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+        end
+      end
+
+      desc 'Get the current application settings' do
+        success Entities::ApplicationSetting
+      end
+      get "application/settings" do
+        present current_settings, with: Entities::ApplicationSetting
+      end
+
+      desc 'Modify application settings' do
+        success Entities::ApplicationSetting
+      end
+      params do
+        optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
+        optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
+        optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
+        optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+        optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+        optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+                                  desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
+        optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+        optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+        optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
+        optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
+        optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
+        optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
+        optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
+        optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
+        optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+        optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
+        optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+        optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
+        given domain_blacklist_enabled: ->(val) { val } do
+          requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+        end
+        optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
+        optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled'
+        optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
+        given require_two_factor_authentication: ->(val) { val } do
+          requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
+        end
+        optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
+        optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
+        optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
+        optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
+        optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
+        given shared_runners_enabled: ->(val) { val } do
+          requires :shared_runners_text, type: String, desc: 'Shared runners text '
+        end
+        optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+        optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+        optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
+        optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
+        given metrics_enabled: ->(val) { val } do
+          requires :metrics_host, type: String, desc: 'The InfluxDB host'
+          requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
+          requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
+          requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
+          requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
+          requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
+          requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
+        end
+        optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
+        given sidekiq_throttling_enabled: ->(val) { val } do
+          requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
+          requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
+        end
+        optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
+        given recaptcha_enabled: ->(val) { val } do
+          requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
+          requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
+        end
+        optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
+        given akismet_enabled: ->(val) { val } do
+          requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
+        end
+        optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
+        optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
+        given sentry_enabled: ->(val) { val } do
+          requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
+        end
+        optional :repository_storage, type: String, desc: 'Storage paths for new projects'
+        optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
+        optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
+        given koding_enabled: ->(val) { val } do
+          requires :koding_url, type: String, desc: 'The Koding team URL'
+        end
+        optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+        given plantuml_enabled: ->(val) { val } do
+          requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+        end
+        optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
+        optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
+        optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
+        optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
+        given housekeeping_enabled: ->(val) { val } do
+          requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
+          requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
+          requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
+          requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
+        end
+        optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+        at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
+                        :default_group_visibility, :restricted_visibility_levels, :import_sources,
+                        :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
+                        :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
+                        :user_oauth_applications, :user_default_external, :signup_enabled,
+                        :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
+                        :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
+                        :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
+                        :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+                        :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
+                        :akismet_enabled, :admin_notification_email, :sentry_enabled,
+                        :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
+                        :version_check_enabled, :email_author_in_body, :html_emails_enabled,
+                        :housekeeping_enabled, :terminal_max_session_time
+      end
+      put "application/settings" do
+        if current_settings.update_attributes(declared_params(include_missing: false))
+          present current_settings, with: Entities::ApplicationSetting
+        else
+          render_validation_error!(current_settings)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
new file mode 100644
index 0000000000000000000000000000000000000000..07dac7e9904563bfee375b85151e477dd0680af3
--- /dev/null
+++ b/lib/api/v3/snippets.rb
@@ -0,0 +1,138 @@
+module API
+  module V3
+    class Snippets < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      resource :snippets do
+        helpers do
+          def snippets_for_current_user
+            SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+          end
+
+          def public_snippets
+            SnippetsFinder.new.execute(current_user, filter: :public)
+          end
+        end
+
+        desc 'Get a snippets list for authenticated user' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          use :pagination
+        end
+        get do
+          present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet
+        end
+
+        desc 'List all public snippets current_user has access to' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          use :pagination
+        end
+        get 'public' do
+          present paginate(public_snippets), with: ::API::Entities::PersonalSnippet
+        end
+
+        desc 'Get a single snippet' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of a snippet'
+        end
+        get ':id' do
+          snippet = snippets_for_current_user.find(params[:id])
+          present snippet, with: ::API::Entities::PersonalSnippet
+        end
+
+        desc 'Create new snippet' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          requires :title, type: String, desc: 'The title of a snippet'
+          requires :file_name, type: String, desc: 'The name of a snippet file'
+          requires :content, type: String, desc: 'The content of a snippet'
+          optional :visibility_level, type: Integer,
+                                      values: Gitlab::VisibilityLevel.values,
+                                      default: Gitlab::VisibilityLevel::INTERNAL,
+                                      desc: 'The visibility level of the snippet'
+        end
+        post do
+          attrs = declared_params(include_missing: false).merge(request: request, api: true)
+          snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+          if snippet.persisted?
+            present snippet, with: ::API::Entities::PersonalSnippet
+          else
+            render_validation_error!(snippet)
+          end
+        end
+
+        desc 'Update an existing snippet' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of a snippet'
+          optional :title, type: String, desc: 'The title of a snippet'
+          optional :file_name, type: String, desc: 'The name of a snippet file'
+          optional :content, type: String, desc: 'The content of a snippet'
+          optional :visibility_level, type: Integer,
+                                      values: Gitlab::VisibilityLevel.values,
+                                      desc: 'The visibility level of the snippet'
+          at_least_one_of :title, :file_name, :content, :visibility_level
+        end
+        put ':id' do
+          snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+          return not_found!('Snippet') unless snippet
+          authorize! :update_personal_snippet, snippet
+
+          attrs = declared_params(include_missing: false)
+
+          UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+          if snippet.persisted?
+            present snippet, with: ::API::Entities::PersonalSnippet
+          else
+            render_validation_error!(snippet)
+          end
+        end
+
+        desc 'Remove snippet' do
+          detail 'This feature was introduced in GitLab 8.15.'
+          success ::API::Entities::PersonalSnippet
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of a snippet'
+        end
+        delete ':id' do
+          snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+          return not_found!('Snippet') unless snippet
+          authorize! :destroy_personal_snippet, snippet
+          snippet.destroy
+          no_content!
+        end
+
+        desc 'Get a raw snippet' do
+          detail 'This feature was introduced in GitLab 8.15.'
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of a snippet'
+        end
+        get ":id/raw" do
+          snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+          return not_found!('Snippet') unless snippet
+
+          env['api.format'] = :txt
+          content_type 'text/plain'
+          present snippet.content
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
index 02a4157c26ead04eb98092122b9f48dd067d6b9c..068750ec07755b0a7e1ecb9291cdde2034f22da8 100644
--- a/lib/api/v3/subscriptions.rb
+++ b/lib/api/v3/subscriptions.rb
@@ -14,7 +14,7 @@ module API
         requires :id, type: String, desc: 'The ID of a project'
         requires :subscribable_id, type: String, desc: 'The ID of a resource'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         subscribable_types.each do |type, finder|
           type_singularized = type.singularize
           entity_class = ::API::Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
index 391510b9ee0bab9e69959b3647d03b10d9351cc7..5787c06fc12ecbfb2b0ef6c10eaf7996a1f35cf3 100644
--- a/lib/api/v3/system_hooks.rb
+++ b/lib/api/v3/system_hooks.rb
@@ -13,6 +13,19 @@ module API
         get do
           present SystemHook.all, with: ::API::Entities::Hook
         end
+
+        desc 'Delete a hook' do
+          success ::API::Entities::Hook
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the system hook'
+        end
+        delete ":id" do
+          hook = SystemHook.find_by(id: params[:id])
+          not_found!('System hook') unless hook
+
+          present hook.destroy, with: ::API::Entities::Hook
+        end
       end
     end
   end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 016e3d8693243003861c8aca8309c13be1ceafdc..c2541de2f5034faec75232c9d4509df148ac1041 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -6,7 +6,7 @@ module API
       params do
         requires :id, type: String, desc: 'The ID of a project'
       end
-      resource :projects do
+      resource :projects, requirements: { id: %r{[^/]+} } do
         desc 'Get a project repository tags' do
           success ::API::Entities::RepoTag
         end
@@ -14,6 +14,26 @@ module API
           tags = user_project.repository.tags.sort_by(&:name).reverse
           present tags, with: ::API::Entities::RepoTag, project: user_project
         end
+
+        desc 'Delete a repository tag'
+        params do
+          requires :tag_name, type: String, desc: 'The name of the tag'
+        end
+        delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
+          authorize_push_project
+
+          result = ::Tags::DestroyService.new(user_project, current_user).
+            execute(params[:tag_name])
+
+          if result[:status] == :success
+            status(200)
+            {
+              tag_name: params[:tag_name]
+            }
+          else
+            render_api_error!(result[:message], result[:return_code])
+          end
+        end
       end
     end
   end
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
new file mode 100644
index 0000000000000000000000000000000000000000..81ae4e8137d54d63baa36334094e39c1215dc212
--- /dev/null
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -0,0 +1,116 @@
+module API
+  module V3
+    module TimeTrackingEndpoints
+      extend ActiveSupport::Concern
+
+      included do
+        helpers do
+          def issuable_name
+            declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+          end
+
+          def issuable_key
+            "#{issuable_name}_id".to_sym
+          end
+
+          def update_issuable_key
+            "update_#{issuable_name}".to_sym
+          end
+
+          def read_issuable_key
+            "read_#{issuable_name}".to_sym
+          end
+
+          def load_issuable
+            @issuable ||= begin
+                            case issuable_name
+                            when 'issue'
+                              find_project_issue(params.delete(issuable_key))
+                            when 'merge_request'
+                              find_project_merge_request(params.delete(issuable_key))
+                            end
+                          end
+          end
+
+          def update_issuable(attrs)
+            custom_params = declared_params(include_missing: false)
+            custom_params.merge!(attrs)
+
+            issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+            if issuable.valid?
+              present issuable, with: ::API::Entities::IssuableTimeStats
+            else
+              render_validation_error!(issuable)
+            end
+          end
+
+          def update_service
+            issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+          end
+        end
+
+        issuable_name            = name.end_with?('Issues') ? 'issue' : 'merge_request'
+        issuable_collection_name = issuable_name.pluralize
+        issuable_key             = "#{issuable_name}_id".to_sym
+
+        desc "Set a time estimate for a project #{issuable_name}"
+        params do
+          requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+          requires :duration, type: String, desc: 'The duration to be parsed'
+        end
+        post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+          authorize! update_issuable_key, load_issuable
+
+          status :ok
+          update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+        end
+
+        desc "Reset the time estimate for a project #{issuable_name}"
+        params do
+          requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+        end
+        post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+          authorize! update_issuable_key, load_issuable
+
+          status :ok
+          update_issuable(time_estimate: 0)
+        end
+
+        desc "Add spent time for a project #{issuable_name}"
+        params do
+          requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+          requires :duration, type: String, desc: 'The duration to be parsed'
+        end
+        post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+          authorize! update_issuable_key, load_issuable
+
+          update_issuable(spend_time: {
+                            duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+                            user: current_user
+                          })
+        end
+
+        desc "Reset spent time for a project #{issuable_name}"
+        params do
+          requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+        end
+        post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+          authorize! update_issuable_key, load_issuable
+
+          status :ok
+          update_issuable(spend_time: { duration: :reset, user: current_user })
+        end
+
+        desc "Show time stats for a project #{issuable_name}"
+        params do
+          requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+        end
+        get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+          authorize! read_issuable_key, load_issuable
+
+          present load_issuable, with: ::API::Entities::IssuableTimeStats
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
index 4f9b5fe72a6a10c32cd47733af92eee226332202..e3b311d61cdbca094fce07dc453e52d249fb7bb0 100644
--- a/lib/api/v3/todos.rb
+++ b/lib/api/v3/todos.rb
@@ -19,8 +19,10 @@ module API
 
         desc 'Mark all todos as done'
         delete do
+          status(200)
+
           todos = TodosFinder.new(current_user, params).execute
-          TodoService.new.mark_todos_as_done(todos, current_user)
+          TodoService.new.mark_todos_as_done(todos, current_user).size
         end
       end
     end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a23d6b6b48cb1546c7a6dd696c7cc53a00a03e46
--- /dev/null
+++ b/lib/api/v3/triggers.rb
@@ -0,0 +1,103 @@
+module API
+  module V3
+    class Triggers < Grape::API
+      include PaginationParams
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Trigger a GitLab project build' do
+          success ::API::V3::Entities::TriggerRequest
+        end
+        params do
+          requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+          requires :token, type: String, desc: 'The unique token of trigger'
+          optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+        end
+        post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
+          project = find_project(params[:id])
+          trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+          not_found! unless project && trigger
+          unauthorized! unless trigger.project == project
+
+          # validate variables
+          variables = params[:variables].to_h
+          unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+            render_api_error!('variables needs to be a map of key-valued strings', 400)
+          end
+
+          # create request and trigger builds
+          trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+          if trigger_request
+            present trigger_request, with: ::API::V3::Entities::TriggerRequest
+          else
+            errors = 'No builds created'
+            render_api_error!(errors, 400)
+          end
+        end
+
+        desc 'Get triggers list' do
+          success ::API::V3::Entities::Trigger
+        end
+        params do
+          use :pagination
+        end
+        get ':id/triggers' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          triggers = user_project.triggers.includes(:trigger_requests)
+
+          present paginate(triggers), with: ::API::V3::Entities::Trigger
+        end
+
+        desc 'Get specific trigger of a project' do
+          success ::API::V3::Entities::Trigger
+        end
+        params do
+          requires :token, type: String, desc: 'The unique token of trigger'
+        end
+        get ':id/triggers/:token' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          trigger = user_project.triggers.find_by(token: params[:token].to_s)
+          return not_found!('Trigger') unless trigger
+
+          present trigger, with: ::API::V3::Entities::Trigger
+        end
+
+        desc 'Create a trigger' do
+          success ::API::V3::Entities::Trigger
+        end
+        post ':id/triggers' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          trigger = user_project.triggers.create
+
+          present trigger, with: ::API::V3::Entities::Trigger
+        end
+
+        desc 'Delete a trigger' do
+          success ::API::V3::Entities::Trigger
+        end
+        params do
+          requires :token, type: String, desc: 'The unique token of trigger'
+        end
+        delete ':id/triggers/:token' do
+          authenticate!
+          authorize! :admin_build, user_project
+
+          trigger = user_project.triggers.find_by(token: params[:token].to_s)
+          return not_found!('Trigger') unless trigger
+
+          trigger.destroy
+
+          present trigger, with: ::API::V3::Entities::Trigger
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
index e05e457a5dfa9554d96d3355dc44c6eff13e1bcb..14f54731730a54488d5b55d5c80fafda9884a32d 100644
--- a/lib/api/v3/users.rb
+++ b/lib/api/v3/users.rb
@@ -71,6 +71,46 @@ module API
             user.activate
           end
         end
+
+        desc 'Get the contribution events of a specified user' do
+          detail 'This feature was introduced in GitLab 8.13.'
+          success ::API::V3::Entities::Event
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the user'
+          use :pagination
+        end
+        get ':id/events' do
+          user = User.find_by(id: params[:id])
+          not_found!('User') unless user
+
+          events = user.events.
+            merge(ProjectsFinder.new.execute(current_user)).
+            references(:project).
+            with_associations.
+            recent
+
+          present paginate(events), with: ::API::V3::Entities::Event
+        end
+
+        desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
+          success ::API::Entities::SSHKey
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the user'
+          requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+        end
+        delete ':id/keys/:key_id' do
+          authenticated_as_admin!
+
+          user = User.find_by(id: params[:id])
+          not_found!('User') unless user
+
+          key = user.keys.find_by(id: params[:key_id])
+          not_found!('Key') unless key
+
+          present key.destroy, with: ::API::Entities::SSHKey
+        end
       end
 
       resource :user do
@@ -90,6 +130,19 @@ module API
         get "emails" do
           present current_user.emails, with: ::API::Entities::Email
         end
+
+        desc 'Delete an SSH key from the currently authenticated user' do
+          success ::API::Entities::SSHKey
+        end
+        params do
+          requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+        end
+        delete "keys/:key_id" do
+          key = current_user.keys.find_by(id: params[:key_id])
+          not_found!('Key') unless key
+
+          present key.destroy, with: ::API::Entities::SSHKey
+        end
       end
     end
   end
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83972b1e7ce9d1fe631828fb9e16373b2a7ec3a2
--- /dev/null
+++ b/lib/api/v3/variables.rb
@@ -0,0 +1,29 @@
+module API
+  module V3
+    class Variables < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+      before { authorize! :admin_build, user_project }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+
+      resource :projects, requirements: { id: %r{[^/]+} } do
+        desc 'Delete an existing variable from a project' do
+          success ::API::Entities::Variable
+        end
+        params do
+          requires :key, type: String, desc: 'The key of the variable'
+        end
+        delete ':id/variables/:key' do
+          variable = user_project.variables.find_by(key: params[:key])
+          not_found!('Variable') unless variable
+
+          present variable.destroy, with: ::API::Entities::Variable
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f623b1dfe9f158af7876072db14416c3bde6566b..5acde41551b107ac9ebe8179f7cd215b80bb3242 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,5 +1,4 @@
 module API
-  # Projects variables API
   class Variables < Grape::API
     include PaginationParams
 
@@ -10,7 +9,7 @@ module API
       requires :id, type: String, desc: 'The ID of a project'
     end
 
-    resource :projects do
+    resource :projects, requirements: { id: %r{[^/]+} } do
       desc 'Get project variables' do
         success Entities::Variable
       end
@@ -81,10 +80,9 @@ module API
       end
       delete ':id/variables/:key' do
         variable = user_project.variables.find_by(key: params[:key])
+        not_found!('Variable') unless variable
 
-        return not_found!('Variable') unless variable
-
-        present variable.destroy, with: Entities::Variable
+        variable.destroy
       end
     end
   end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 22319ec6623f4379c15ab074ff3e74082328f9d9..4016ac7634869da2450607b9a2c5c755dfd6c862 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -5,7 +5,7 @@ module Backup
     attr_reader :config, :db_file_name
 
     def initialize
-      @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
+      @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
       @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
     end
 
@@ -13,28 +13,32 @@ module Backup
       FileUtils.mkdir_p(File.dirname(db_file_name))
       FileUtils.rm_f(db_file_name)
       compress_rd, compress_wr = IO.pipe
-      compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+      compress_pid = spawn(*%w(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
       compress_rd.close
 
-      dump_pid = case config["adapter"]
-      when /^mysql/ then
-        $progress.print "Dumping MySQL database #{config['database']} ... "
-        # Workaround warnings from MySQL 5.6 about passwords on cmd line
-        ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
-        spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
-      when "postgresql" then
-        $progress.print "Dumping PostgreSQL database #{config['database']} ... "
-        pg_env
-        pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
-        if Gitlab.config.backup.pg_schema
-          pgsql_args << "-n"
-          pgsql_args << Gitlab.config.backup.pg_schema
+      dump_pid =
+        case config["adapter"]
+        when /^mysql/ then
+          $progress.print "Dumping MySQL database #{config['database']} ... "
+          # Workaround warnings from MySQL 5.6 about passwords on cmd line
+          ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+          spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
+        when "postgresql" then
+          $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+          pg_env
+          pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+          if Gitlab.config.backup.pg_schema
+            pgsql_args << "-n"
+            pgsql_args << Gitlab.config.backup.pg_schema
+          end
+          spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
         end
-        spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
-      end
       compress_wr.close
 
-      success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+      success = [compress_pid, dump_pid].all? do |pid|
+        Process.waitpid(pid)
+        $?.success?
+      end
 
       report_success(success)
       abort 'Backup failed' unless success
@@ -42,23 +46,27 @@ module Backup
 
     def restore
       decompress_rd, decompress_wr = IO.pipe
-      decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+      decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
       decompress_wr.close
 
-      restore_pid = case config["adapter"]
-      when /^mysql/ then
-        $progress.print "Restoring MySQL database #{config['database']} ... "
-        # Workaround warnings from MySQL 5.6 about passwords on cmd line
-        ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
-        spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
-      when "postgresql" then
-        $progress.print "Restoring PostgreSQL database #{config['database']} ... "
-        pg_env
-        spawn('psql', config['database'], in: decompress_rd)
-      end
+      restore_pid =
+        case config["adapter"]
+        when /^mysql/ then
+          $progress.print "Restoring MySQL database #{config['database']} ... "
+          # Workaround warnings from MySQL 5.6 about passwords on cmd line
+          ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+          spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
+        when "postgresql" then
+          $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+          pg_env
+          spawn('psql', config['database'], in: decompress_rd)
+        end
       decompress_rd.close
 
-      success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+      success = [decompress_pid, restore_pid].all? do |pid|
+        Process.waitpid(pid)
+        $?.success?
+      end
 
       report_success(success)
       abort 'Restore failed' unless success
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 247c32c1c0ae92e129de83a3a09fe9d4ff289477..30a91647b779a42c0f2fc1fc04cc9e85641d6847 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -26,10 +26,10 @@ module Backup
           abort 'Backup failed'
         end
 
-        run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+        run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
         FileUtils.rm_rf(@backup_files_dir)
       else
-        run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+        run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
       end
     end
 
@@ -37,7 +37,7 @@ module Backup
       backup_existing_files_dir
       create_files_dir
 
-      run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+      run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
     end
 
     def backup_existing_files_dir
@@ -47,7 +47,7 @@ module Backup
       end
     end
 
-    def run_pipeline!(cmd_list, options={})
+    def run_pipeline!(cmd_list, options = {})
       status_list = Open3.pipeline(*cmd_list, options)
       abort 'Backup failed' unless status_list.compact.all?(&:success?)
     end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index f099c0651ac8e22cd9a45ae1f9b587ba29cac3be..7b4476fa4db1f0606a006e94277e7f18ee80a38a 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,8 +1,8 @@
 module Backup
   class Manager
-    ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
-    FOLDERS_TO_BACKUP = %w[repositories db]
-    FILE_NAME_SUFFIX = '_gitlab_backup.tar'
+    ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
+    FOLDERS_TO_BACKUP = %w[repositories db].freeze
+    FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
 
     def pack
       # Make sure there is a connection
@@ -20,13 +20,13 @@ module Backup
       Dir.chdir(Gitlab.config.backup.path) do
         File.open("#{Gitlab.config.backup.path}/backup_information.yml",
                   "w+") do |file|
-          file << s.to_yaml.gsub(/^---\n/,'')
+          file << s.to_yaml.gsub(/^---\n/, '')
         end
 
         # create archive
         $progress.print "Creating backup archive: #{tar_file} ... "
         # Set file permissions on open to prevent chmod races.
-        tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
+        tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
         if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
           $progress.puts "done".color(:green)
         else
@@ -50,8 +50,9 @@ module Backup
       directory = connect_to_remote_directory(connection_settings)
 
       if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
-          multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
-          encryption: Gitlab.config.backup.upload.encryption)
+                                multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+                                encryption: Gitlab.config.backup.upload.encryption,
+                                storage_class: Gitlab.config.backup.upload.storage_class)
         $progress.puts "done".color(:green)
       else
         puts "uploading backup to #{remote_directory} failed".color(:red)
@@ -123,11 +124,11 @@ module Backup
         exit 1
       end
 
-      if ENV['BACKUP'].present?
-        tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
-      else
-        tar_file = file_list.first
-      end
+      tar_file = if ENV['BACKUP'].present?
+                   "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+                 else
+                   file_list.first
+                 end
 
       unless File.exist?(tar_file)
         $progress.puts "The backup file #{tar_file} does not exist!"
@@ -158,7 +159,7 @@ module Backup
     end
 
     def tar_version
-      tar_version, _ = Gitlab::Popen.popen(%W(tar --version))
+      tar_version, _ = Gitlab::Popen.popen(%w(tar --version))
       tar_version.force_encoding('locale').split("\n").first
     end
 
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 91e43dcb1145df3693803b0c2b22269795426394..cd745d35e7cfd0ed5f78e4be9d89ef40ab617fde 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,7 +2,7 @@ require 'yaml'
 
 module Backup
   class Repository
-
+    # rubocop:disable Metrics/AbcSize
     def dump
       prepare
 
@@ -68,7 +68,8 @@ module Backup
     end
 
     def restore
-      Gitlab.config.repositories.storages.each do |name, path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        path = repository_storage['path']
         next unless File.exist?(path)
 
         # Move repos dir to 'repositories.old' dir
@@ -85,11 +86,11 @@ module Backup
 
         project.ensure_dir_exist
 
-        if File.exists?(path_to_project_bundle)
-          cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
-        else
-          cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
-        end
+        cmd = if File.exist?(path_to_project_bundle)
+                %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
+              else
+                %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
+              end
 
         output, status = Gitlab::Popen.popen(cmd)
         if status.zero?
@@ -150,6 +151,7 @@ module Backup
         puts output
       end
     end
+    # rubocop:enable Metrics/AbcSize
 
     protected
 
@@ -179,9 +181,8 @@ module Backup
       return unless Dir.exist?(path)
 
       dir_entries = Dir.entries(path)
-      %w[annex custom_hooks].each do |entry|
-        yield(entry) if dir_entries.include?(entry)
-      end
+
+      yield('custom_hooks') if dir_entries.include?('custom_hooks')
     end
 
     def prepare
@@ -193,13 +194,13 @@ module Backup
     end
 
     def silent
-      {err: '/dev/null', out: '/dev/null'}
+      { err: '/dev/null', out: '/dev/null' }
     end
 
     private
 
     def repository_storage_paths_args
-      Gitlab.config.repositories.storages.values
+      Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
     end
   end
 end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 9261f77f3c9d45ee137dd450ba5a6c401267c592..35118375499f53e553d8577af47b909c5b21574d 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,6 @@ require 'backup/files'
 
 module Backup
   class Uploads < Files
-
     def initialize
       super('uploads', Rails.root.join('public/uploads'))
     end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 3b15ff6566fc72293977d2f22b788ed0d02f47ad..8bc2dd18bdadd2ae37c93fbbdedf1cedca6f3089 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -160,11 +160,12 @@ module Banzai
 
             data = data_attributes_for(link_content || match, project, object, link: !!link_content)
 
-            if matches.names.include?("url") && matches[:url]
-              url = matches[:url]
-            else
-              url = url_for_object_cached(object, project)
-            end
+            url =
+              if matches.names.include?("url") && matches[:url]
+                matches[:url]
+              else
+                url_for_object_cached(object, project)
+              end
 
             content = link_content || object_link_text(object, matches)
 
@@ -238,18 +239,13 @@ module Banzai
       # path.
       def projects_per_reference
         @projects_per_reference ||= begin
-          hash = {}
           refs = Set.new
 
           references_per_project.each do |project_ref, _|
             refs << project_ref
           end
 
-          find_projects_for_paths(refs.to_a).each do |project|
-            hash[project.path_with_namespace] = project
-          end
-
-          hash
+          find_projects_for_paths(refs.to_a).index_by(&:full_path)
         end
       end
 
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 80c844baecd92d42f3acf581ccfbda416620caee..b8d2673c1a67f7739adb81c35368ec1660d0a638 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -37,7 +37,7 @@ module Banzai
         and contains(., '://')
         and not(starts-with(., 'http'))
         and not(starts-with(., 'ftp'))
-      ])
+      ]).freeze
 
       def call
         return doc if context[:autolink] == false
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index a8c1ca0c60a8a88d8171a961d614d9a9481fca47..d6138816e70127d2f957b5a76ecb679ed4519cca 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -17,8 +17,8 @@ module Banzai
 
           next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
 
-          html = emoji_name_image_filter(content)
-          html = emoji_unicode_image_filter(html)
+          html = emoji_unicode_element_unicode_filter(content)
+          html = emoji_name_element_unicode_filter(html)
 
           next if html == content
 
@@ -27,33 +27,30 @@ module Banzai
         doc
       end
 
-      # Replace :emoji: with corresponding images.
+      # Replace :emoji: with corresponding gl-emoji unicode.
       #
       # text - String text to replace :emoji: in.
       #
-      # Returns a String with :emoji: replaced with images.
-      def emoji_name_image_filter(text)
+      # Returns a String with :emoji: replaced with gl-emoji unicode.
+      def emoji_name_element_unicode_filter(text)
         text.gsub(emoji_pattern) do |match|
           name = $1
-          emoji_image_tag(name, emoji_url(name))
+          Gitlab::Emoji.gl_emoji_tag(name)
         end
       end
 
-      # Replace unicode emoji with corresponding images if they exist.
+      # Replace unicode emoji with corresponding gl-emoji unicode.
       #
       # text - String text to replace unicode emoji in.
       #
-      # Returns a String with unicode emoji replaced with images.
-      def emoji_unicode_image_filter(text)
+      # Returns a String with unicode emoji replaced with gl-emoji unicode.
+      def emoji_unicode_element_unicode_filter(text)
         text.gsub(emoji_unicode_pattern) do |moji|
-          emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+          emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
+          Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
         end
       end
 
-      def emoji_image_tag(emoji_name, emoji_url)
-        "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
-      end
-
       # Build a regexp that matches all valid :emoji: names.
       def self.emoji_pattern
         @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
@@ -66,52 +63,13 @@ module Banzai
 
       private
 
-      def emoji_url(name)
-        emoji_path = emoji_filename(name)
-
-        if context[:asset_host]
-          # Asset host is specified.
-          url_to_image(emoji_path)
-        elsif context[:asset_root]
-          # Gitlab url is specified
-          File.join(context[:asset_root], url_to_image(emoji_path))
-        else
-          # All other cases
-          url_to_image(emoji_path)
-        end
-      end
-
-      def emoji_unicode_url(moji)
-        emoji_unicode_path = emoji_unicode_filename(moji)
-
-        if context[:asset_host]
-          url_to_image(emoji_unicode_path)
-        elsif context[:asset_root]
-          File.join(context[:asset_root], url_to_image(emoji_unicode_path))
-        else
-          url_to_image(emoji_unicode_path)
-        end
-      end
-
-      def url_to_image(image)
-        ActionController::Base.helpers.url_to_image(image)
-      end
-
       def emoji_pattern
         self.class.emoji_pattern
       end
 
-      def emoji_filename(name)
-        "#{Gitlab::Emoji.emoji_filename(name)}.png"
-      end
-
       def emoji_unicode_pattern
         self.class.emoji_unicode_pattern
       end
-
-      def emoji_unicode_filename(name)
-        "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
-      end
     end
   end
 end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index d08267a9d6cc8e23dca71f0f4b62f78f9dc869d5..0ea4eeaed5bfc4f6a736c97cf9abb6541827a4ae 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -149,11 +149,12 @@ module Banzai
           name, reference = *parts.compact.map(&:strip)
         end
 
-        if url?(reference)
-          href = reference
-        else
-          href = ::File.join(project_wiki_base_path, reference)
-        end
+        href =
+          if url?(reference)
+            reference
+          else
+            ::File.join(project_wiki_base_path, reference)
+          end
 
         content_tag(:a, name || reference, href: href, class: 'gfm')
       end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index f0fb6084a35e3cea7cd7ae8a24bee854469e1e17..123c92fd2500b3906dc9197489d76cb4ca61bc83 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -2,29 +2,22 @@ module Banzai
   module Filter
     # HTML filter that wraps links around inline images.
     class ImageLinkFilter < HTML::Pipeline::Filter
-      
       # Find every image that isn't already wrapped in an `a` tag, create
       # a new node (a link to the image source), copy the image as a child
       # of the anchor, and then replace the img with the link-wrapped version.
       def call
         doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
-          div = doc.document.create_element(
-            'div',
-            class: 'image-container'
-          )
-
           link = doc.document.create_element(
             'a',
             class: 'no-attachment-icon',
             href: img['src'],
-            target: '_blank'
+            target: '_blank',
+            rel: 'noopener noreferrer'
           )
 
           link.children = img.clone
 
-          div.children = link
-
-          img.replace(div)
+          img.replace(link)
         end
 
         doc
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index fd6b970413218d6dbb7017eb3d91cb86141b1dde..044d18ff8241ef951a72af7f4253733f68c8f447 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -39,11 +39,12 @@ module Banzai
           projects_per_reference.each do |path, project|
             issue_ids = references_per_project[path]
 
-            if project.default_issues_tracker?
-              issues = project.issues.where(iid: issue_ids.to_a)
-            else
-              issues = issue_ids.map { |id| ExternalIssue.new(id, project) }
-            end
+            issues =
+              if project.default_issues_tracker?
+                project.issues.where(iid: issue_ids.to_a)
+              else
+                issue_ids.map { |id| ExternalIssue.new(id, project) }
+              end
 
             issues.each do |issue|
               hash[project][issue.iid.to_i] = issue
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index af1e575fc89d6a9270c9e07a58a9eba7fda372b4..d5f9e252f62b854564395117d275c779b8866611 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -35,6 +35,10 @@ module Banzai
         # Allow span elements
         whitelist[:elements].push('span')
 
+        # Allow html5 details/summary elements
+        whitelist[:elements].push('details')
+        whitelist[:elements].push('summary')
+
         # Allow abbr elements with title attribute
         whitelist[:elements].push('abbr')
         whitelist[:attributes]['abbr'] = %w(title)
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index a447e2b8bff1332869775c3168d1e18f26f39084..9f09ca90697ae32ff3542eca65c00927657d367a 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -5,8 +5,6 @@ module Banzai
     # HTML Filter to highlight fenced code blocks
     #
     class SyntaxHighlightFilter < HTML::Pipeline::Filter
-      include Rouge::Plugins::Redcarpet
-
       def call
         doc.search('pre > code').each do |node|
           highlight_node(node)
@@ -23,7 +21,7 @@ module Banzai
         lang = lexer.tag
 
         begin
-          code = format(lex(lexer, code))
+          code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
 
           css_classes << " js-syntax-highlight #{lang}"
         rescue
@@ -45,10 +43,6 @@ module Banzai
         lexer.lex(code)
       end
 
-      def format(tokens)
-        rouge_formatter.format(tokens)
-      end
-
       def lexer_for(language)
         (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
       end
@@ -57,11 +51,6 @@ module Banzai
         # Replace the parent `pre` element with the entire highlighted block
         node.parent.replace(highlighted)
       end
-
-      # Override Rouge::Plugins::Redcarpet#rouge_formatter
-      def rouge_formatter(lexer = nil)
-        @rouge_formatter ||= Rouge::Formatters::HTML.new
-      end
     end
   end
 end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index c973897f420d60ae53bd2a7ddc932ae726c101bb..fe1f092313607630421982c11c630b593befd890 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -74,10 +74,7 @@ module Banzai
       # The keys of this Hash are the namespace paths, the values the
       # corresponding Namespace objects.
       def namespaces
-        @namespaces ||=
-          Namespace.where_full_path_in(usernames).each_with_object({}) do |row, hash|
-            hash[row.full_path] = row
-          end
+        @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path)
       end
 
       # Returns all usernames referenced in the current document.
@@ -133,7 +130,7 @@ module Banzai
         data = data_attribute(group: namespace.id)
         content = link_content || Group.reference_prefix + group
 
-        link_tag(url, data, content, namespace.name)
+        link_tag(url, data, content, namespace.full_name)
       end
 
       def link_to_user(user, namespace, link_content: nil)
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index b64a1287d4ded2337b3aa436d81e19f935aab4ec..35cb10eae5dd068bd00bd3141c2314bdaafe8905 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -43,6 +43,7 @@ module Banzai
           element['title'] || element['alt'],
           href: element['src'],
           target: '_blank',
+          rel: 'noopener noreferrer',
           title: "Download '#{element['title'] || element['alt']}'")
         download_paragraph = doc.document.create_element('p')
         download_paragraph.children = link
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index b25d6f18d599a0d5ae9feaac6c9f69102a53fcc6..fd4a6a107c2846754c523f3cd9c00fa49790ad0f 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -2,10 +2,10 @@ module Banzai
   module Pipeline
     class GfmPipeline < BasePipeline
       # These filters convert GitLab Flavored Markdown (GFM) to HTML.
-      # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
+      # The handlers defined in app/assets/javascripts/copy_as_gfm.js
       # consequently convert that same HTML to GFM to be copied to the clipboard.
       # Every filter that generates HTML from GFM should have a handler in
-      # app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
+      # app/assets/javascripts/copy_as_gfm.js, in reverse order.
       # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
       def self.filters
         @filters ||= FilterArray[
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 2058a58d0ae93413cadb1e7e202b75d3d35ece54..52fdb9a214023921321a65f1782102507e3acc3e 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -134,9 +134,7 @@ module Banzai
         ids = unique_attribute_values(nodes, attribute)
         rows = collection_objects_for_ids(collection, ids)
 
-        rows.each_with_object({}) do |row, hash|
-          hash[row.id] = row
-        end
+        rows.index_by(&:id)
       end
 
       # Returns an Array containing all unique values of an attribute of the
@@ -210,7 +208,7 @@ module Banzai
           grouped_objects_for_nodes(nodes, Project, 'data-project')
       end
 
-      def can?(user, permission, subject)
+      def can?(user, permission, subject = :global)
         Ability.allowed?(user, permission, subject)
       end
 
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 7e55cf4deabfc2b338fdd12a76cc6e260b41bb68..b9279c33f5b420b08a9ab890a751a81e4b4a7a9f 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -1,8 +1,8 @@
 module Bitbucket
   class Connection
-    DEFAULT_API_VERSION = '2.0'
-    DEFAULT_BASE_URI    = 'https://api.bitbucket.org/'
-    DEFAULT_QUERY       = {}
+    DEFAULT_API_VERSION = '2.0'.freeze
+    DEFAULT_BASE_URI    = 'https://api.bitbucket.org/'.freeze
+    DEFAULT_QUERY       = {}.freeze
 
     attr_reader :expires_at, :expires_in, :refresh_token, :token
 
@@ -24,9 +24,7 @@ module Bitbucket
       response.parsed
     end
 
-    def expired?
-      connection.expired?
-    end
+    delegate :expired?, to: :connection
 
     def refresh!
       response = connection.refresh!
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
index 5e2eb57bb0ee37dad8158dcaf6bcf001e5e0ebf4..efe10542f199c3a218068b8b780530744c77d2dd 100644
--- a/lib/bitbucket/error/unauthorized.rb
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -1,6 +1,5 @@
 module Bitbucket
   module Error
-    class Unauthorized < StandardError
-    end
+    Unauthorized = Class.new(StandardError)
   end
 end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 423eff8f2a5ae98fa9434d5193868b27337910ff..59b0fda8e142cff1616bad7a28b044757e0242d2 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -23,7 +23,7 @@ module Bitbucket
         url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
 
         if token.present?
-          clone_url = URI::parse(url)
+          clone_url = URI.parse(url)
           clone_url.user = "x-token-auth:#{token}"
           clone_url.to_s
         else
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 158a33f26fec65d95f744b0bceb6347c5b516f46..b3ccad7b28d2b3241f602f16ea645b72fb2106af 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -13,7 +13,7 @@ module Ci
       5 => 'magenta',
       6 => 'cyan',
       7 => 'white', # not that this is gray in the dark (aka default) color table
-    }
+    }.freeze
 
     STYLE_SWITCHES = {
       bold:       0x01,
@@ -21,7 +21,7 @@ module Ci
       underline:  0x04,
       conceal:    0x08,
       cross:      0x10,
-    }
+    }.freeze
 
     def self.convert(ansi, state = nil)
       Converter.new.convert(ansi, state)
@@ -29,64 +29,108 @@ module Ci
 
     class Converter
       def on_0(s) reset()                            end
+
       def on_1(s) enable(STYLE_SWITCHES[:bold])      end
+
       def on_3(s) enable(STYLE_SWITCHES[:italic])    end
+
       def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
       def on_8(s) enable(STYLE_SWITCHES[:conceal])   end
+
       def on_9(s) enable(STYLE_SWITCHES[:cross])     end
 
       def on_21(s) disable(STYLE_SWITCHES[:bold])      end
+
       def on_22(s) disable(STYLE_SWITCHES[:bold])      end
+
       def on_23(s) disable(STYLE_SWITCHES[:italic])    end
+
       def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
       def on_28(s) disable(STYLE_SWITCHES[:conceal])   end
+
       def on_29(s) disable(STYLE_SWITCHES[:cross])     end
 
       def on_30(s) set_fg_color(0) end
+
       def on_31(s) set_fg_color(1) end
+
       def on_32(s) set_fg_color(2) end
+
       def on_33(s) set_fg_color(3) end
+
       def on_34(s) set_fg_color(4) end
+
       def on_35(s) set_fg_color(5) end
+
       def on_36(s) set_fg_color(6) end
+
       def on_37(s) set_fg_color(7) end
+
       def on_38(s) set_fg_color_256(s) end
+
       def on_39(s) set_fg_color(9) end
 
       def on_40(s) set_bg_color(0) end
+
       def on_41(s) set_bg_color(1) end
+
       def on_42(s) set_bg_color(2) end
+
       def on_43(s) set_bg_color(3) end
+
       def on_44(s) set_bg_color(4) end
+
       def on_45(s) set_bg_color(5) end
+
       def on_46(s) set_bg_color(6) end
+
       def on_47(s) set_bg_color(7) end
+
       def on_48(s) set_bg_color_256(s) end
+
       def on_49(s) set_bg_color(9) end
 
       def on_90(s) set_fg_color(0, 'l') end
+
       def on_91(s) set_fg_color(1, 'l') end
+
       def on_92(s) set_fg_color(2, 'l') end
+
       def on_93(s) set_fg_color(3, 'l') end
+
       def on_94(s) set_fg_color(4, 'l') end
+
       def on_95(s) set_fg_color(5, 'l') end
+
       def on_96(s) set_fg_color(6, 'l') end
+
       def on_97(s) set_fg_color(7, 'l') end
+
       def on_99(s) set_fg_color(9, 'l') end
 
       def on_100(s) set_bg_color(0, 'l') end
+
       def on_101(s) set_bg_color(1, 'l') end
+
       def on_102(s) set_bg_color(2, 'l') end
+
       def on_103(s) set_bg_color(3, 'l') end
+
       def on_104(s) set_bg_color(4, 'l') end
+
       def on_105(s) set_bg_color(5, 'l') end
+
       def on_106(s) set_bg_color(6, 'l') end
+
       def on_107(s) set_bg_color(7, 'l') end
+
       def on_109(s) set_bg_color(9, 'l') end
 
       attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
 
-      STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
+      STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
 
       def convert(raw, new_state)
         reset_state
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 8b939663ffd972360a4cdda9dceb273515120ea8..746e76a1b1f1d8b9ed5ab6aedbb6435df58e8fd5 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -24,7 +24,7 @@ module Ci
 
           new_update = current_runner.ensure_runner_queue_value
 
-          result = Ci::RegisterBuildService.new(current_runner).execute
+          result = Ci::RegisterJobService.new(current_runner).execute
 
           if result.valid?
             if result.build
@@ -167,7 +167,10 @@ module Ci
 
           build.artifacts_file = artifacts
           build.artifacts_metadata = metadata
-          build.artifacts_expire_in = params['expire_in']
+          build.artifacts_expire_in =
+            params['expire_in'] ||
+            Gitlab::CurrentSettings.current_application_settings
+              .default_artifacts_expire_in
 
           if build.save
             present(build, with: Entities::BuildDetails)
@@ -214,6 +217,7 @@ module Ci
           build = Ci::Build.find_by_id(params[:id])
           authenticate_build!(build)
 
+          status(200)
           build.erase_artifacts!
         end
       end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 5ff25a3a9b24d534d9229bd1ea0c3c806cff8322..996990b464f805c0859036cfaeee9bc8036d161c 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,7 +1,7 @@
 module Ci
   module API
     module Helpers
-      BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+      BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
       BUILD_TOKEN_PARAM = :token
       UPDATE_RUNNER_EVERY = 10 * 60
 
@@ -60,7 +60,7 @@ module Ci
       end
 
       def build_not_found!
-        if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+        if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
           no_content!
         else
           not_found!
@@ -73,7 +73,7 @@ module Ci
 
       def get_runner_version_from_params
         return unless params["info"].present?
-        attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+        attributes_for_keys(%w(name version revision platform architecture), params["info"])
       end
 
       def max_artifacts_size
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index bcc82969eb3f0e0f97eca45335d9f17a4d741dcd..45aa2adccf56297f2b42a7ca11e674612e4d1d25 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -1,44 +1,38 @@
 module Ci
   module API
-    # Runners API
     class Runners < Grape::API
       resource :runners do
-        # Delete runner
-        # Parameters:
-        #   token (required) - The unique token of runner
-        #
-        # Example Request:
-        #   GET /runners/delete
+        desc 'Delete a runner'
+        params do
+          requires :token, type: String, desc: 'The unique token of the runner'
+        end
         delete "delete" do
-          required_attributes! [:token]
           authenticate_runner!
+
+          status(200)
           Ci::Runner.find_by_token(params[:token]).destroy
         end
 
-        # Register a new runner
-        #
-        # Note: This is an "internal" API called when setting up
-        # runners, so it is authenticated differently.
-        #
-        # Parameters:
-        #   token (required) - The unique token of runner
-        #
-        # Example Request:
-        #   POST /runners/register
+        desc 'Register a new runner' do
+          success Entities::Runner
+        end
+        params do
+          requires :token, type: String, desc: 'The unique token of the runner'
+          optional :description, type: String, desc: 'The description of the runner'
+          optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
+          optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
+          optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
+        end
         post "register" do
-          required_attributes! [:token]
-
-          attributes = attributes_for_keys(
-            [:description, :tag_list, :run_untagged, :locked]
-          )
+          runner_params = declared(params, include_missing: false).except(:token)
 
           runner =
             if runner_registration_token_valid?
               # Create shared runner. Requires admin access
-              Ci::Runner.create(attributes.merge(is_shared: true))
+              Ci::Runner.create(runner_params.merge(is_shared: true))
             elsif project = Project.find_by(runners_token: params[:token])
               # Create a specific runner for project.
-              project.runners.create(attributes)
+              project.runners.create(runner_params)
             end
 
           return forbidden! unless runner
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 649ee4d018b4db4e5e793151678274fba79c22e5..15a461a16dd19ef746940a5ab0a140e83b7a1d1b 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -1,6 +1,6 @@
 module Ci
   class GitlabCiYamlProcessor
-    class ValidationError < StandardError; end
+    ValidationError = Class.new(StandardError)
 
     include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
 
@@ -58,7 +58,7 @@ module Ci
         commands: job[:commands],
         tag_list: job[:tags] || [],
         name: job[:name].to_s,
-        allow_failure: job[:allow_failure] || false,
+        allow_failure: job[:ignore],
         when: job[:when] || 'on_success',
         environment: job[:environment_name],
         coverage_regex: job[:coverage],
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2cbb7bfb67d1e6dc3fc2a98236cde4dab7022e96..196cdd36a88c34b1a4ddb3542afbec3fbbeb6fc2 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -5,7 +5,7 @@ module ContainerRegistry
   class Client
     attr_accessor :uri
 
-    MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+    MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'.freeze
 
     # Taken from: FaradayMiddleware::FollowRedirects
     REDIRECT_CODES = Set.new [301, 302, 303, 307]
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 82551f1f2223767fea1b16ab48e4553fb8dab363..dd864eea3fa09c8ff72d8a1977fa8800686de710 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -2,7 +2,7 @@
 # file path string when combined in a request parameter
 module ExtractsPath
   # Raised when given an invalid file path
-  class InvalidPathError < StandardError; end
+  InvalidPathError = Class.new(StandardError)
 
   # Given a string containing both a Git tree-ish, such as a branch or tag, and
   # a filesystem path joined by forward slashes, attempts to separate the two.
@@ -42,7 +42,7 @@ module ExtractsPath
 
     return pair unless @project
 
-    if id.match(/^([[:alnum:]]{40})(.+)/)
+    if id =~ /^(\h{40})(.+)/
       # If the ref appears to be a SHA, we're done, just split the string
       pair = $~.captures
     else
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 440dd44ece72bd628f52ff14bd20bda3344edf4a..eb19ab45ac3444f655fc78a357d6b0477dde9f11 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -32,9 +32,9 @@ class FileSizeValidator < ActiveModel::EachValidator
   end
 
   def validate_each(record, attribute, value)
-    raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
+    raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.is_a? CarrierWave::Uploader::Base
 
-    value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)
+    value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.is_a?(String)
 
     CHECKS.each do |key, validity_check|
       next unless check_value = options[key]
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 9b484a2ecfd9ddfbb3a31a9f36357cf6fee801ea..8c28009b9c6518acde431540fe6374d0964833c8 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,7 +5,7 @@
 #
 module Gitlab
   module Access
-    class AccessDeniedError < StandardError; end
+    AccessDeniedError = Class.new(StandardError)
 
     NO_ACCESS = 0
     GUEST     = 10
@@ -21,9 +21,7 @@ module Gitlab
     PROTECTION_DEV_CAN_MERGE = 3
 
     class << self
-      def values
-        options.values
-      end
+      delegate :values, to: :options
 
       def all_values
         options_with_owner.values
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
index f48abcc86d53ac932898619b76c35a361dcffaa6..e4f7cad2b7902954d663866a2efbf31db92bd794 100644
--- a/lib/gitlab/allowable.rb
+++ b/lib/gitlab/allowable.rb
@@ -1,6 +1,6 @@
 module Gitlab
   module Allowable
-    def can?(user, action, subject)
+    def can?(user, action, subject = :global)
       Ability.allowed?(user, action, subject)
     end
   end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index f638905a1e07ef515af68b589e47470899da48a6..eee5601b0edaee146226aa15936615decab6979b 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,10 +1,18 @@
 module Gitlab
   module Auth
-    class MissingPersonalTokenError < StandardError; end
+    MissingPersonalTokenError = Class.new(StandardError)
 
-    SCOPES = [:api, :read_user]
-    DEFAULT_SCOPES = [:api]
-    OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+    # Scopes used for GitLab API access
+    API_SCOPES = [:api, :read_user].freeze
+
+    # Scopes used for OpenID Connect
+    OPENID_SCOPES = [:openid].freeze
+
+    # Default scopes for OAuth applications that don't define their own
+    DEFAULT_SCOPES = [:api].freeze
+
+    # Other available scopes
+    OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
 
     class << self
       def find_for_git_client(login, password, project:, ip:)
@@ -18,27 +26,30 @@ module Gitlab
           build_access_token_check(login, password) ||
           lfs_token_check(login, password) ||
           oauth_access_token_check(login, password) ||
-          personal_access_token_check(login, password) ||
           user_with_password_for_git(login, password) ||
+          personal_access_token_check(password) ||
           Gitlab::Auth::Result.new
 
         rate_limit!(ip, success: result.success?, login: login)
+        Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
 
         result
       end
 
       def find_with_user_password(login, password)
-        user = User.by_login(login)
+        Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+          user = User.by_login(login)
 
-        # If no user is found, or it's an LDAP server, try LDAP.
-        #   LDAP users are only authenticated via LDAP
-        if user.nil? || user.ldap_user?
-          # Second chance - try LDAP authentication
-          return nil unless Gitlab::LDAP::Config.enabled?
+          # If no user is found, or it's an LDAP server, try LDAP.
+          #   LDAP users are only authenticated via LDAP
+          if user.nil? || user.ldap_user?
+            # Second chance - try LDAP authentication
+            return nil unless Gitlab::LDAP::Config.enabled?
 
-          Gitlab::LDAP::Authentication.login(login, password)
-        else
-          user if user.valid_password?(password)
+            Gitlab::LDAP::Authentication.login(login, password)
+          else
+            user if user.active? && user.valid_password?(password)
+          end
         end
       end
 
@@ -102,14 +113,13 @@ module Gitlab
         end
       end
 
-      def personal_access_token_check(login, password)
-        if login && password
-          token = PersonalAccessToken.active.find_by_token(password)
-          validation = User.by_login(login)
+      def personal_access_token_check(password)
+        return unless password.present?
 
-          if valid_personal_access_token?(token, validation)
-            Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
-          end
+        token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
+
+        if token && valid_api_token?(token)
+          Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
         end
       end
 
@@ -117,10 +127,6 @@ module Gitlab
         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
diff --git a/lib/gitlab/auth/too_many_ips.rb b/lib/gitlab/auth/too_many_ips.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed8627915519c7470f6be8aec00c97be5bc9549e
--- /dev/null
+++ b/lib/gitlab/auth/too_many_ips.rb
@@ -0,0 +1,17 @@
+module Gitlab
+  module Auth
+    class TooManyIps < StandardError
+      attr_reader :user_id, :ip, :unique_ips_count
+
+      def initialize(user_id, ip, unique_ips_count)
+        @user_id = user_id
+        @ip = ip
+        @unique_ips_count = unique_ips_count
+      end
+
+      def message
+        "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}"
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bf2239ca150e0dbba7d8878240712a2a6b7fc4df
--- /dev/null
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+  module Auth
+    class UniqueIpsLimiter
+      USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze
+
+      class << self
+        def limit_user_id!(user_id)
+          if config.unique_ips_limit_enabled
+            ip = RequestContext.client_ip
+            unique_ips = update_and_return_ips_count(user_id, ip)
+
+            raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
+          end
+        end
+
+        def limit_user!(user = nil)
+          user ||= yield if block_given?
+          limit_user_id!(user.id) unless user.nil?
+          user
+        end
+
+        def config
+          Gitlab::CurrentSettings.current_application_settings
+        end
+
+        def update_and_return_ips_count(user_id, ip)
+          time = Time.now.utc.to_i
+          key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}"
+
+          Gitlab::Redis.with do |redis|
+            unique_ips_count = nil
+            redis.multi do |r|
+              r.zadd(key, time, ip)
+              r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window)
+              unique_ips_count = r.zcard(key)
+            end
+            unique_ips_count.value
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
deleted file mode 100644
index 39b43ab5489a362ed9cfc3da7ba83209d653ce35..0000000000000000000000000000000000000000
--- a/lib/gitlab/award_emoji.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module Gitlab
-  class AwardEmoji
-    CATEGORIES = {
-      objects: "Objects",
-      travel: "Travel",
-      symbols: "Symbols",
-      nature: "Nature",
-      people: "People",
-      activity: "Activity",
-      flags: "Flags",
-      food: "Food"
-    }.with_indifferent_access
-
-    def self.normalize_emoji_name(name)
-      aliases[name] || name
-    end
-
-    def self.emoji_by_category
-      unless @emoji_by_category
-        @emoji_by_category = Hash.new { |h, key| h[key] = [] }
-
-        emojis.each do |emoji_name, data|
-          data["name"] = emoji_name
-
-          # Skip Fitzpatrick(tone) modifiers
-          next if data["category"] == "modifier"
-
-          category = data["category"]
-
-          @emoji_by_category[category] << data
-        end
-
-        @emoji_by_category = @emoji_by_category.sort.to_h
-      end
-
-      @emoji_by_category
-    end
-
-    def self.emojis
-      @emojis ||=
-        begin
-          json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
-          JSON.parse(File.read(json_path))
-        end
-    end
-
-    def self.aliases
-      @aliases ||=
-        begin
-          json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
-          JSON.parse(File.read(json_path))
-        end
-    end
-
-    # Returns an Array of Emoji names and their asset URLs.
-    def self.urls
-      @urls ||= begin
-                  path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
-                  # Construct the full asset path ourselves because
-                  # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
-                  # of entries since it has to do a lot of extra work (e.g. regexps).
-                  prefix = Gitlab::Application.config.assets.prefix
-                  digest = Gitlab::Application.config.assets.digest
-                  base =
-                    if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
-                      Gitlab::Application.config.relative_url_root
-                    else
-                      ''
-                    end
-
-                  JSON.parse(File.read(path)).map do |hash|
-                    if digest
-                      fname = "#{hash['unicode']}-#{hash['digest']}"
-                    else
-                      fname = hash['unicode']
-                    end
-
-                    { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
-                  end
-                end
-    end
-  end
-end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index 2b95ddfcb53e6ecd308575bc7c94a4c94a98a675..bc0e0cd441dd7badf28b968c4f5f595cfdc30dc9 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -15,7 +15,7 @@ module Gitlab
           canceled: '#9f9f9f',
           skipped: '#9f9f9f',
           unknown: '#9f9f9f'
-        }
+        }.freeze
 
         def initialize(badge)
           @entity = badge.entity
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index 06e0d084e9f1fd7bc4ffd85f5a19213591881943..fcecb1d9665e558cabe15fd24f57ff9c1ea26aa1 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -13,7 +13,7 @@ module Gitlab
           medium: '#dfb317',
           low: '#e05d44',
           unknown: '#9f9f9f'
-        }
+        }.freeze
 
         def initialize(badge)
           @entity = badge.entity
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 95308aca95f788cadfb7190650b587bf4e44a6ed..5b32fca00a45c688feb04ff25768ee7aa825ec1c 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -5,7 +5,7 @@ module Gitlab
     attr_reader :raw_changes
 
     def initialize(changes)
-      @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+      @raw_changes = changes.is_a?(String) ? changes.lines : changes
     end
 
     def each(&block)
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
index a0058407fb24eac492240d87cbc6524e1e3e42b7..054f7f4be0ce6679f86a17ed9d52a74396c4aba1 100644
--- a/lib/gitlab/chat_commands/presenters/issue_base.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -32,7 +32,7 @@ module Gitlab
             },
             {
               title: "Labels",
-              value: @resource.labels.any? ? @resource.label_names : "_None_",
+              value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
               short: true
             }
           ]
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 273118135a90a142664309c4176430e5a5968ebe..c85f79127bc8d3d029adea0896b22fa7937402de 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,16 +1,20 @@
 module Gitlab
   module Checks
     class ChangeAccess
-      attr_reader :user_access, :project, :skip_authorization
+      # protocol is currently used only in EE
+      attr_reader :user_access, :project, :skip_authorization, :protocol
 
       def initialize(
-        change, user_access:, project:, env: {}, skip_authorization: false)
+        change, user_access:, project:, env: {}, skip_authorization: false,
+        protocol:
+      )
         @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
         @branch_name = Gitlab::Git.branch_name(@ref)
         @user_access = user_access
         @project = project
         @env = env
         @skip_authorization = skip_authorization
+        @protocol = protocol
       end
 
       def exec
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index cd2e83b4c27e96e7b41ad3777a4ff97832a596e8..a375ccbece0b35b9ef195f584c9387520c55c7bb 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -6,7 +6,7 @@ module Gitlab
     module Build
       module Artifacts
         class Metadata
-          class ParserError < StandardError; end
+          ParserError = Class.new(StandardError)
 
           VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
           INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 7f4c750b6fdaf8973296135c86be7a739764bc70..6f799c2f031b0235b70c92be0733a973e43303ae 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -27,6 +27,8 @@ module Gitlab
           end
         end
 
+        delegate :empty?, to: :children
+
         def directory?
           blank_node? || @path.end_with?('/')
         end
@@ -91,10 +93,6 @@ module Gitlab
           blank_node? || @entries.include?(@path)
         end
 
-        def empty?
-          children.empty?
-        end
-
         def total_size
           descendant_pattern = %r{^#{Regexp.escape(@path)}}
           entries.sum do |path, entry|
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c62aeb60fa95ebe148b14f09ebf8c0869a5799da
--- /dev/null
+++ b/lib/gitlab/ci/build/image.rb
@@ -0,0 +1,33 @@
+module Gitlab
+  module Ci
+    module Build
+      class Image
+        attr_reader :name
+
+        class << self
+          def from_image(job)
+            image = Gitlab::Ci::Build::Image.new(job.options[:image])
+            return unless image.valid?
+            image
+          end
+
+          def from_services(job)
+            services = job.options[:services].to_a.map do |service|
+              Gitlab::Ci::Build::Image.new(service)
+            end
+
+            services.select(&:valid?).compact
+          end
+        end
+
+        def initialize(image)
+          @name = image
+        end
+
+        def valid?
+          @name.present?
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1877429ac464c98bfae2e5bb8ed9c3af946fe1fb
--- /dev/null
+++ b/lib/gitlab/ci/build/step.rb
@@ -0,0 +1,46 @@
+module Gitlab
+  module Ci
+    module Build
+      class Step
+        WHEN_ON_FAILURE = 'on_failure'.freeze
+        WHEN_ON_SUCCESS = 'on_success'.freeze
+        WHEN_ALWAYS = 'always'.freeze
+
+        attr_reader :name
+        attr_writer :script
+        attr_accessor :timeout, :when, :allow_failure
+
+        class << self
+          def from_commands(job)
+            self.new(:script).tap do |step|
+              step.script = job.commands
+              step.timeout = job.timeout
+              step.when = WHEN_ON_SUCCESS
+            end
+          end
+
+          def from_after_script(job)
+            after_script = job.options[:after_script]
+            return unless after_script
+
+            self.new(:after_script).tap do |step|
+              step.script = after_script
+              step.timeout = job.timeout
+              step.when = WHEN_ALWAYS
+              step.allow_failure = true
+            end
+          end
+        end
+
+        def initialize(name)
+          @name = name
+          @allow_failure = false
+        end
+
+        def script
+          @script.split("\n")
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index b756b0d4555dbbe71be89e4741b88c28e3cd75ee..8275aacee9bf22fd5064e3195bf438d18c6de2b9 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -9,7 +9,7 @@ module Gitlab
           include Validatable
           include Attributable
 
-          ALLOWED_KEYS = %i[name untracked paths when expire_in]
+          ALLOWED_KEYS = %i[name untracked paths when expire_in].freeze
 
           attributes ALLOWED_KEYS
 
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 7653cab668b8c080b90459f13b2eea13bed2acf7..f074df9c7a1d789bfc813c094fc5020adec612aa 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -8,7 +8,7 @@ module Gitlab
         class Cache < Node
           include Configurable
 
-          ALLOWED_KEYS = %i[key untracked paths]
+          ALLOWED_KEYS = %i[key untracked paths].freeze
 
           validations do
             validates :config, allowed_keys: ALLOWED_KEYS
@@ -22,6 +22,12 @@ module Gitlab
 
           entry :paths, Entry::Paths,
             description: 'Specify which paths should be cached across builds.'
+
+          helpers :key
+
+          def value
+            super.merge(key: key_value)
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index f7c530c7d9fa584275c4ae827ccd22feddd5997a..0c1f9eb7cbfa339ace2e7316a3711d7145a59f2d 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
         class Environment < Node
           include Validatable
 
-          ALLOWED_KEYS = %i[name url action on_stop]
+          ALLOWED_KEYS = %i[name url action on_stop].freeze
 
           validations do
             validate do
@@ -21,12 +21,14 @@ module Gitlab
             validates :name,
               type: {
                 with: String,
-                message: Gitlab::Regex.environment_name_regex_message }
+                message: Gitlab::Regex.environment_name_regex_message
+              }
 
             validates :name,
               format: {
                 with: Gitlab::Regex.environment_name_regex,
-                message: Gitlab::Regex.environment_name_regex_message }
+                message: Gitlab::Regex.environment_name_regex_message
+              }
 
             with_options if: :hash? do
               validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 9f5e393d191c493156e2511a70052bc00de3a213..6be8288748f7eb649ed3843bf71e3fbd512f36d5 100644
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -6,7 +6,7 @@ module Gitlab
         # Factory class responsible for fabricating entry objects.
         #
         class Factory
-          class InvalidFactory < StandardError; end
+          InvalidFactory = Class.new(StandardError)
 
           def initialize(entry)
             @entry = entry
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 69a5e6f433d119d6bdf92283d7c039e951688ec4..176301bcca1bf98e1dedeecffad18ef2139de311 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
 
           ALLOWED_KEYS = %i[tags script only except type image services allow_failure
                             type stage when artifacts cache dependencies before_script
-                            after_script variables environment coverage]
+                            after_script variables environment coverage].freeze
 
           validations do
             validates :config, allowed_keys: ALLOWED_KEYS
@@ -104,6 +104,14 @@ module Gitlab
             (before_script_value.to_a + script_value.to_a).join("\n")
           end
 
+          def manual_action?
+            self.when == 'manual'
+          end
+
+          def ignored?
+            allow_failure.nil? ? manual_action? : allow_failure
+          end
+
           private
 
           def inherit!(deps)
@@ -135,7 +143,8 @@ module Gitlab
               environment_name: environment_defined? ? environment_value[:name] : nil,
               coverage: coverage_defined? ? coverage_value : nil,
               artifacts: artifacts_value,
-              after_script: after_script_value }
+              after_script: after_script_value,
+              ignore: ignored? }
           end
         end
       end
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0e4c9fe6edcd665367f7c520ecb1f805f44a02b6..f27ad0a7759893bf2fa4483a3838c1b4d62ca6e0 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -11,6 +11,10 @@ module Gitlab
           validations do
             validates :config, key: true
           end
+
+          def self.default
+            'default'
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index 5eef2868cd69ee12447e9378617f368bfe918c22..a6a914d79c1e1a7cab7d4781cb29b96ca0ad42f2 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -6,7 +6,7 @@ module Gitlab
         # Base abstract class for each configuration entry node.
         #
         class Node
-          class InvalidError < StandardError; end
+          InvalidError = Class.new(StandardError)
 
           attr_reader :config, :metadata
           attr_accessor :key, :parent, :description
@@ -70,6 +70,12 @@ module Gitlab
             true
           end
 
+          def inspect
+            val = leaf? ? config : descendants
+            unspecified = specified? ? '' : '(unspecified) '
+            "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
+          end
+
           def self.default
           end
 
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index b33b8238230c179d6d96c43bda28a228ba14fb1a..1171ac10f226b984d1f159e952525c8cc752668d 100644
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -29,6 +29,10 @@ module Gitlab
           def relevant?
             false
           end
+
+          def inspect
+            "#<#{self.class.name}>"
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
index dbf6eb0edbe6625f60db0111822cf9b482919e05..e7d9f6a77610009073694731d802f8560de28ab8 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/ci/config/loader.rb
@@ -2,7 +2,7 @@ module Gitlab
   module Ci
     class Config
       class Loader
-        class FormatError < StandardError; end
+        FormatError = Class.new(StandardError)
 
         def initialize(config)
           @config = YAML.safe_load(config, [Symbol], [], true)
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 0f4b7b24cefadee97dd3f048c378e25ddb86474f..3495b8d0448b923d4f8b9592c47ebaffbe7e6ab7 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -5,22 +5,10 @@ module Gitlab
         class Play < SimpleDelegator
           include Status::Extended
 
-          def text
-            'manual'
-          end
-
           def label
             'manual play action'
           end
 
-          def icon
-            'icon_status_manual'
-          end
-
-          def group
-            'manual'
-          end
-
           def has_action?
             can?(user, :update_build, subject)
           end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index 90401cad0d27398b986751bcbc0892546e5d2516..e8530f2aaaed4ad041e792ee73f08d53e2574441 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -5,22 +5,10 @@ module Gitlab
         class Stop < SimpleDelegator
           include Status::Extended
 
-          def text
-            'manual'
-          end
-
           def label
             'manual stop action'
           end
 
-          def icon
-            'icon_status_manual'
-          end
-
-          def group
-            'manual'
-          end
-
           def has_action?
             can?(user, :update_build, subject)
           end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5f28521901df6ac2c0965dae962b636cb4290fe8
--- /dev/null
+++ b/lib/gitlab/ci/status/manual.rb
@@ -0,0 +1,19 @@
+module Gitlab
+  module Ci
+    module Status
+      class Manual < Status::Core
+        def text
+          'manual'
+        end
+
+        def label
+          'manual action'
+        end
+
+        def icon
+          'icon_status_manual'
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a250c3fcb419dd8ebf4a57fef68e4676c376cab1
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -0,0 +1,23 @@
+module Gitlab
+  module Ci
+    module Status
+      module Pipeline
+        class Blocked < SimpleDelegator
+          include Status::Extended
+
+          def text
+            'blocked'
+          end
+
+          def label
+            'waiting for manual action'
+          end
+
+          def self.matches?(pipeline, user)
+            pipeline.blocked?
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 13c8343b12a2b6170fab96d8b9323f2d3eded876..17f9a75f43669ffcebdc92104b92a7c6bfaf4978 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -4,7 +4,8 @@ module Gitlab
       module Pipeline
         class Factory < Status::Factory
           def self.extended_statuses
-            [Status::SuccessWarning]
+            [[Status::SuccessWarning,
+              Status::Pipeline::Blocked]]
           end
 
           def self.common_helpers
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index c843315782dc1eb167c4dfecda4f99024c75af1f..75a213ef752d100258d4486bfed5376643e5212f 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -4,8 +4,7 @@ module Gitlab
       include Gitlab::Routing.url_helpers
       include IconsHelper
 
-      class MissingResolution < ResolutionError
-      end
+      MissingResolution = Class.new(ResolutionError)
 
       CONTEXT_LINES = 3
 
@@ -91,11 +90,12 @@ module Gitlab
         our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
 
         lines.each do |line|
-          if line.type == 'old'
-            line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
-          else
-            line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
-          end
+          line.rich_text =
+            if line.type == 'old'
+              their_highlight[line.old_line - 1].try(:html_safe)
+            else
+              our_highlight[line.new_line - 1].try(:html_safe)
+            end
         end
       end
 
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index fa5bd4649d473c619d1a4d75baaf1b703892aaed..990b719ecfdf24d6d9bf6be66531064538d4f924 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -1,8 +1,7 @@
 module Gitlab
   module Conflict
     class FileCollection
-      class ConflictSideMissing < StandardError
-      end
+      ConflictSideMissing = Class.new(StandardError)
 
       attr_reader :merge_request, :our_commit, :their_commit
 
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index ddd657903fb6ab08625218bc37ece897ec98143c..84f9ecd3d23fe0958909deb47b312acfbf7ea3aa 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -1,35 +1,23 @@
 module Gitlab
   module Conflict
     class Parser
-      class UnresolvableError < StandardError
-      end
-
-      class UnmergeableFile < UnresolvableError
-      end
-
-      class UnsupportedEncoding < UnresolvableError
-      end
+      UnresolvableError = Class.new(StandardError)
+      UnmergeableFile = Class.new(UnresolvableError)
+      UnsupportedEncoding = Class.new(UnresolvableError)
 
       # Recoverable errors - the conflict can be resolved in an editor, but not with
       # sections.
-      class ParserError < StandardError
-      end
-
-      class UnexpectedDelimiter < ParserError
-      end
-
-      class MissingEndDelimiter < ParserError
-      end
+      ParserError = Class.new(StandardError)
+      UnexpectedDelimiter = Class.new(ParserError)
+      MissingEndDelimiter = Class.new(ParserError)
 
       def parse(text, our_path:, their_path:, parent_file: nil)
         raise UnmergeableFile if text.blank? # Typically a binary file
         raise UnmergeableFile if text.length > 200.kilobytes
 
-        begin
-          text.to_json
-        rescue Encoding::UndefinedConversionError
-          raise UnsupportedEncoding
-        end
+        text.force_encoding('UTF-8')
+
+        raise UnsupportedEncoding unless text.valid_encoding?
 
         line_obj_index = 0
         line_old = 1
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
index a0f2006bc245b7bc22811d11d85dda466a0fb9a6..0b61256b35a39e9be4448bed890484ddb5f94598 100644
--- a/lib/gitlab/conflict/resolution_error.rb
+++ b/lib/gitlab/conflict/resolution_error.rb
@@ -1,6 +1,5 @@
 module Gitlab
   module Conflict
-    class ResolutionError < StandardError
-    end
+    ResolutionError = Class.new(StandardError)
   end
 end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index e20f5f6f5143f747e483a51844a37520fd3357d5..82576d197fedcd4214b154364852d4400eb5a80f 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -25,9 +25,7 @@ module Gitlab
       settings || in_memory_application_settings
     end
 
-    def sidekiq_throttling_enabled?
-      current_application_settings.sidekiq_throttling_enabled?
-    end
+    delegate :sidekiq_throttling_enabled?, to: :current_application_settings
 
     def in_memory_application_settings
       @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index e50e54b6e99f89154ea1e880326d7db49cd8373b..182a30fd74d8286c18277b9a25f0217e2decd80a 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -39,7 +39,7 @@ module Gitlab
           started_at: build.started_at,
           finished_at: build.finished_at,
           when: build.when,
-          manual: build.manual?,
+          manual: build.action?,
           user: build.user.try(:hook_attrs),
           runner: build.runner && runner_hook_attrs(build.runner),
           artifacts_file: {
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a47d7e98a625f7b7df562d5894fdc1e83b9dd9c9..63b8d0d3b9d1fdb267799c0051af876899893d5c 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
     # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
     MAX_INT_VALUE = 2147483647
 
+    def self.config
+      ActiveRecord::Base.configurations[Rails.env]
+    end
+
     def self.adapter_name
-      ActiveRecord::Base.configurations[Rails.env]['adapter']
+      config['adapter']
     end
 
     def self.mysql?
@@ -24,7 +28,7 @@ module Gitlab
     def self.nulls_last_order(field, direction = 'ASC')
       order = "#{field} #{direction}"
 
-      if Gitlab::Database.postgresql?
+      if postgresql?
         order << ' NULLS LAST'
       else
         # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -38,7 +42,7 @@ module Gitlab
     def self.nulls_first_order(field, direction = 'ASC')
       order = "#{field} #{direction}"
 
-      if Gitlab::Database.postgresql?
+      if postgresql?
         order << ' NULLS FIRST'
       else
         # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -50,7 +54,7 @@ module Gitlab
     end
 
     def self.random
-      Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+      postgresql? ? "RANDOM()" : "RAND()"
     end
 
     def true_value
@@ -79,11 +83,16 @@ module Gitlab
       end
     end
 
-    def self.create_connection_pool(pool_size)
+    # pool_size - The size of the DB pool.
+    # host - An optional host name to use instead of the default one.
+    def self.create_connection_pool(pool_size, host = nil)
       # See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb
       env = Rails.env
       original_config = ActiveRecord::Base.configurations
+
       env_config = original_config[env].merge('pool' => pool_size)
+      env_config['host'] = host if host
+
       config = original_config.merge(env => env_config)
 
       spec =
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 08607c27c0987470f9131a188faa44257811de6d..23890e5f4935dc94b55562d76278a86ad3bf7c89 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -108,6 +108,7 @@ module Gitlab
 
         Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
       end
+
       # Need to cast '0' to an INTERVAL before we can check if the interval is positive
       def zero_interval
         Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 9ea976e18fae04401dd3d26b4303289843bb0201..7db896522a917ac33867b08230c60a49f167cde4 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -50,7 +50,7 @@ module Gitlab
         # Only update text if line is found. This will prevent
         # issues with submodules given the line only exists in diff content.
         if rich_line
-          line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+          line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' '
           "#{line_prefix}#{rich_line}".html_safe
         end
       end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 87a9b1e23acc41dd82017a5e9526877b1a6e6b51..736933b1c4b0a6cdbc33cd3a04005f0d0dec9087 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -4,7 +4,7 @@ module Gitlab
       MARKDOWN_SYMBOLS = {
         addition: "+",
         deletion: "-"
-      }
+      }.freeze
 
       attr_accessor :raw_line, :rich_line
 
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 80a146b4a5a96f490b0252b5404232486ad8a4aa..114656958e3c3594b5d92e8fa4ae52688632cab5 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -38,11 +38,11 @@ module Gitlab
       end
 
       def added?
-        type == 'new'
+        type == 'new' || type == 'new-nonewline'
       end
 
       def removed?
-        type == 'old'
+        type == 'old' || type == 'old-nonewline'
       end
 
       def rich_text
@@ -52,7 +52,7 @@ module Gitlab
       end
 
       def meta?
-        type == 'match' || type == 'nonewline'
+        type == 'match'
       end
 
       def as_json(opts = nil)
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 89320f5d9dc4342bb3266a1ec6c603bf29ed4b9e..742f989c50b40f35c6fc630dcd88d0626556ed3d 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -11,6 +11,7 @@ module Gitlab
         line_old = 1
         line_new = 1
         type = nil
+        context = nil
 
         # By returning an Enumerator we make it possible to search for a single line (with #find)
         # without having to instantiate all the others that come after it.
@@ -20,7 +21,7 @@ module Gitlab
 
             full_line = line.delete("\n")
 
-            if line.match(/^@@ -/)
+            if line =~ /^@@ -/
               type = "match"
 
               line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
@@ -31,7 +32,8 @@ module Gitlab
               line_obj_index += 1
               next
             elsif line[0] == '\\'
-              type = 'nonewline'
+              type = "#{context}-nonewline"
+
               yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
               line_obj_index += 1
             else
@@ -43,8 +45,10 @@ module Gitlab
             case line[0]
             when "+"
               line_new += 1
+              context = :new
             when "-"
               line_old += 1
+              context = :old
             when "\\" # rubocop:disable Lint/EmptyWhen
               # No increment
             else
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index ecf62dead350fd54c4f45079829a75201e62ee5f..fc728123c97c61b05b0bc0f31bc4a32aeee005b8 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -140,15 +140,16 @@ module Gitlab
 
       def find_diff_file(repository)
         # We're at the initial commit, so just get that as we can't compare to anything.
-        if Gitlab::Git.blank_ref?(start_sha)
-          compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
-        else
-          compare = Gitlab::Git::Compare.new(
-            repository.raw_repository,
-            start_sha,
-            head_sha
-          )
-        end
+        compare =
+          if Gitlab::Git.blank_ref?(start_sha)
+            Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+          else
+            Gitlab::Git::Compare.new(
+              repository.raw_repository,
+              start_sha,
+              head_sha
+            )
+          end
 
         diff = compare.diffs(paths: paths).first
 
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 40a4815a9a02ba320d8ab71c4e075ffe96be9f20..543e62794c59be36685de6300ddd5b4fd2cee028 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -3,8 +3,8 @@ module Gitlab
     class Message
       attr_reader :path, :offline
 
-      OFFLINE = "\e[31moffline\e[0m"
-      ONLINE = "\e[32monline\e[0m"
+      OFFLINE = "\e[31moffline\e[0m".freeze
+      ONLINE = "\e[32monline\e[0m".freeze
 
       # path - The file path of the migration.
       # offline - When set to `true` the migration will require downtime.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c8e36d8ff4aae064fc8b11b4448ac6fdbda1fbbd..e0fdf3f3d641c00d0727ee4cf83e77114084e799 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -119,7 +119,7 @@ module Gitlab
       step("Reseting to latest master", %w[git reset --hard origin/master])
 
       step("Checking if #{patch_path} applies cleanly to EE/master")
-      output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
+      output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}])
 
       unless status.zero?
         failed_files = output.lines.reduce([]) do |memo, line|
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd2f5d3615eba9c75e6087e4a9666e7e4f906edb..35ea2e0ef594172c0324ccebda0e1e277f66fc0a 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -5,7 +5,7 @@ require 'gitlab/email/handler/unsubscribe_handler'
 module Gitlab
   module Email
     module Handler
-      HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
+      HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
 
       def self.for(mail, mail_key)
         HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index b64db5d01ae43e44d1c899e4688855d324e8ac63..ec0529b5a4b20c3cd1fe446cac276fa5c8a1f399 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -4,19 +4,19 @@ require_dependency 'gitlab/email/handler'
 # Inspired in great part by Discourse's Email::Receiver
 module Gitlab
   module Email
-    class ProcessingError < StandardError; end
-    class EmailUnparsableError < ProcessingError; end
-    class SentNotificationNotFoundError < ProcessingError; end
-    class ProjectNotFound < ProcessingError; end
-    class EmptyEmailError < ProcessingError; end
-    class AutoGeneratedEmailError < ProcessingError; end
-    class UserNotFoundError < ProcessingError; end
-    class UserBlockedError < ProcessingError; end
-    class UserNotAuthorizedError < ProcessingError; end
-    class NoteableNotFoundError < ProcessingError; end
-    class InvalidNoteError < ProcessingError; end
-    class InvalidIssueError < ProcessingError; end
-    class UnknownIncomingEmail < ProcessingError; end
+    ProcessingError = Class.new(StandardError)
+    EmailUnparsableError = Class.new(ProcessingError)
+    SentNotificationNotFoundError = Class.new(ProcessingError)
+    ProjectNotFound = Class.new(ProcessingError)
+    EmptyEmailError = Class.new(ProcessingError)
+    AutoGeneratedEmailError = Class.new(ProcessingError)
+    UserNotFoundError = Class.new(ProcessingError)
+    UserBlockedError = Class.new(ProcessingError)
+    UserNotAuthorizedError = Class.new(ProcessingError)
+    NoteableNotFoundError = Class.new(ProcessingError)
+    InvalidNoteError = Class.new(ProcessingError)
+    InvalidIssueError = Class.new(ProcessingError)
+    UnknownIncomingEmail = Class.new(ProcessingError)
 
     class Receiver
       def initialize(raw)
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 8c8dd1b9cef492395cd0d216d2b8d3a9c3200885..558df87f36d44f1550401acfa68e2fbfa5e92ea8 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -31,11 +31,12 @@ module Gitlab
       private
 
       def select_body(message)
-        if message.multipart?
-          part = message.text_part || message.html_part || message
-        else
-          part = message
-        end
+        part =
+          if message.multipart?
+            message.text_part || message.html_part || message
+          else
+            message
+          end
 
         decoded = fix_charset(part)
 
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index bbbca8acc40316a618c4ca6ae86f839e63bb8be6..a16d9fc2265be6e3d02b1f546880b5354fbc395c 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -1,7 +1,7 @@
 module Gitlab
   module Emoji
     extend self
-    
+
     def emojis
       Gemojione.index.instance_variable_get(:@emoji_by_name)
     end
@@ -18,6 +18,10 @@ module Gitlab
       emojis.keys
     end
 
+    def emojis_aliases
+      @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
+    end
+
     def emoji_filename(name)
       emojis[name]["unicode"]
     end
@@ -25,5 +29,32 @@ module Gitlab
     def emoji_unicode_filename(moji)
       emojis_by_moji[moji]["unicode"]
     end
+
+    def emoji_unicode_version(name)
+      @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+      @emoji_unicode_versions_by_name[name]
+    end
+
+    def normalize_emoji_name(name)
+      emojis_aliases[name] || name
+    end
+
+    def emoji_image_tag(name, src)
+      "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
+    end
+
+    # CSS sprite fallback takes precedence over image fallback
+    def gl_emoji_tag(name)
+      emoji_name = emojis_aliases[name] || name
+      emoji_info = emojis[emoji_name]
+      return unless emoji_info
+
+      data = {
+        name: emoji_name,
+        unicode_version: emoji_unicode_version(emoji_name)
+      }
+
+      ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], data: data)
+    end
   end
 end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ffbc6e17dc598e4353cdfc5486dd4f9516ed1722
--- /dev/null
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -0,0 +1,66 @@
+module Gitlab
+  module EtagCaching
+    class Middleware
+      RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
+      ROUTE_REGEXP = Regexp.union(
+        %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+      )
+
+      def initialize(app)
+        @app = app
+      end
+
+      def call(env)
+        return @app.call(env) unless enabled_for_current_route?(env)
+        Gitlab::Metrics.add_event(:etag_caching_middleware_used)
+
+        etag, cached_value_present = get_etag(env)
+        if_none_match = env['HTTP_IF_NONE_MATCH']
+
+        if if_none_match == etag
+          Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+          [304, { 'ETag' => etag }, ['']]
+        else
+          track_cache_miss(if_none_match, cached_value_present)
+
+          status, headers, body = @app.call(env)
+          headers['ETag'] = etag
+          [status, headers, body]
+        end
+      end
+
+      private
+
+      def enabled_for_current_route?(env)
+        ROUTE_REGEXP.match(env['PATH_INFO'])
+      end
+
+      def get_etag(env)
+        cache_key = env['PATH_INFO']
+        store = Gitlab::EtagCaching::Store.new
+        current_value = store.get(cache_key)
+        cached_value_present = current_value.present?
+
+        unless cached_value_present
+          current_value = store.touch(cache_key, only_if_missing: true)
+        end
+
+        [weak_etag_format(current_value), cached_value_present]
+      end
+
+      def weak_etag_format(value)
+        %Q{W/"#{value}"}
+      end
+
+      def track_cache_miss(if_none_match, cached_value_present)
+        if if_none_match.blank?
+          Gitlab::Metrics.add_event(:etag_caching_header_missing)
+        elsif !cached_value_present
+          Gitlab::Metrics.add_event(:etag_caching_key_not_found)
+        else
+          Gitlab::Metrics.add_event(:etag_caching_resource_changed)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9532e432f787cede6cfd3bf0a7544c987989db8a
--- /dev/null
+++ b/lib/gitlab/etag_caching/store.rb
@@ -0,0 +1,32 @@
+module Gitlab
+  module EtagCaching
+    class Store
+      EXPIRY_TIME = 10.minutes
+      REDIS_NAMESPACE = 'etag:'.freeze
+
+      def get(key)
+        Gitlab::Redis.with { |redis| redis.get(redis_key(key)) }
+      end
+
+      def touch(key, only_if_missing: false)
+        etag = generate_etag
+
+        Gitlab::Redis.with do |redis|
+          redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing)
+        end
+
+        etag
+      end
+
+      private
+
+      def generate_etag
+        SecureRandom.hex
+      end
+
+      def redis_key(key)
+        "#{REDIS_NAMESPACE}#{key}"
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2dd427043962581265f199dd0dc389960827a847..62ddd45785d90780a677957c8efc957bc466464e 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -10,7 +10,7 @@ module Gitlab
   # ExclusiveLease.
   #
   class ExclusiveLease
-    LUA_CANCEL_SCRIPT = <<-EOS
+    LUA_CANCEL_SCRIPT = <<-EOS.freeze
       local key, uuid = KEYS[1], ARGV[1]
       if redis.call("get", key) == uuid then
         redis.call("del", key)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 1d93a67dc56a4a1aec85abb716647e77edcb3243..c9ca4cadd1c414298a45f17cbb15bff9d4415e1c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
       koding: '.koding.yml',
       gitlab_ci: '.gitlab-ci.yml',
       avatar: /\Alogo\.(png|jpg|gif)\z/
-    }
+    }.freeze
 
     # Returns an Array of file types based on the given paths.
     #
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b742d9e1e4bd2ba35ac9b7887301702b9931f2e0..e56eb0d3beb9d8cc24bddff9e65049106ce173f9 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -93,163 +93,6 @@ module Gitlab
             commit_id: sha,
           )
         end
-
-        # Commit file in repository and return commit sha
-        #
-        # options should contain next structure:
-        #   file: {
-        #     content: 'Lorem ipsum...',
-        #     path: 'documents/story.txt',
-        #     update: true
-        #   },
-        #   author: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   committer: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   commit: {
-        #     message: 'Wow such commit',
-        #     branch: 'master',
-        #     update_ref: false
-        #   }
-        #
-        # rubocop:disable Metrics/AbcSize
-        # rubocop:disable Metrics/CyclomaticComplexity
-        # rubocop:disable Metrics/PerceivedComplexity
-        def commit(repository, options, action = :add)
-          file = options[:file]
-          update = file[:update].nil? ? true : file[:update]
-          author = options[:author]
-          committer = options[:committer]
-          commit = options[:commit]
-          repo = repository.rugged
-          ref = commit[:branch]
-          update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
-          parents = []
-          mode = 0o100644
-
-          unless ref.start_with?('refs/')
-            ref = 'refs/heads/' + ref
-          end
-
-          path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
-          # Abort if any invalid characters remain (e.g. ../foo)
-          raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
-
-          filename = path_name.to_s
-          index = repo.index
-
-          unless repo.empty?
-            rugged_ref = repo.references[ref]
-            raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
-            last_commit = rugged_ref.target
-            index.read_tree(last_commit.tree)
-            parents = [last_commit]
-          end
-
-          if action == :remove
-            index.remove(filename)
-          else
-            file_entry = index.get(filename)
-
-            if action == :rename
-              old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
-              old_filename = old_path_name.to_s
-              file_entry = index.get(old_filename)
-              index.remove(old_filename) unless file_entry.blank?
-            end
-
-            if file_entry
-              raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
-
-              # Preserve the current file mode if one is available
-              mode = file_entry[:mode] if file_entry[:mode]
-            end
-
-            content = file[:content]
-            detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
-
-            unless detect && detect[:type] == :binary
-              # When writing to the repo directly as we are doing here,
-              # the `core.autocrlf` config isn't taken into account.
-              content.gsub!("\r\n", "\n") if repository.autocrlf
-            end
-
-            oid = repo.write(content, :blob)
-            index.add(path: filename, oid: oid, mode: mode)
-          end
-
-          opts = {}
-          opts[:tree] = index.write_tree(repo)
-          opts[:author] = author
-          opts[:committer] = committer
-          opts[:message] = commit[:message]
-          opts[:parents] = parents
-          opts[:update_ref] = ref if update_ref
-
-          Rugged::Commit.create(repo, opts)
-        end
-        # rubocop:enable Metrics/AbcSize
-        # rubocop:enable Metrics/CyclomaticComplexity
-        # rubocop:enable Metrics/PerceivedComplexity
-
-        # Remove file from repository and return commit sha
-        #
-        # options should contain next structure:
-        #   file: {
-        #     path: 'documents/story.txt'
-        #   },
-        #   author: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   committer: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   commit: {
-        #     message: 'Remove FILENAME',
-        #     branch: 'master'
-        #   }
-        #
-        def remove(repository, options)
-          commit(repository, options, :remove)
-        end
-
-        # Rename file from repository and return commit sha
-        #
-        # options should contain next structure:
-        #   file: {
-        #     previous_path: 'documents/old_story.txt'
-        #     path: 'documents/story.txt'
-        #     content: 'Lorem ipsum...',
-        #     update: true
-        #   },
-        #   author: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   committer: {
-        #     email: 'user@example.com',
-        #     name: 'Test User',
-        #     time: Time.now
-        #   },
-        #   commit: {
-        #     message: 'Rename FILENAME',
-        #     branch: 'master'
-        #   }
-        #
-        def rename(repository, options)
-          commit(repository, options, :rename)
-        end
       end
 
       def initialize(options)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index d785516ebdda1dad1dfdf71c99271b4c6047ffa0..3a73697dc5d9d5b389de99c89cb5e13bcc1cdc82 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,6 +14,8 @@ module Gitlab
 
       attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
 
+      delegate :tree, to: :raw_commit
+
       def ==(other)
         return false unless other.is_a?(Gitlab::Git::Commit)
 
@@ -218,10 +220,6 @@ module Gitlab
         raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
       end
 
-      def tree
-        raw_commit.tree
-      end
-
       def stats
         Gitlab::Git::CommitStats.new(self)
       end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index d6b3b5705a9a764c23bc24b8a1a551ad1675769f..019be15135309bdb434bc9085de707ca559ae2f9 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -2,7 +2,7 @@
 module Gitlab
   module Git
     class Diff
-      class TimeoutError < StandardError; end
+      TimeoutError = Class.new(StandardError)
       include Gitlab::Git::EncodingHelper
 
       # Diff properties
@@ -176,9 +176,13 @@ module Gitlab
       def initialize(raw_diff, collapse: false)
         case raw_diff
         when Hash
-          init_from_hash(raw_diff, collapse: collapse)
+          init_from_hash(raw_diff)
+          prune_diff_if_eligible(collapse)
         when Rugged::Patch, Rugged::Diff::Delta
           init_from_rugged(raw_diff, collapse: collapse)
+        when Gitaly::CommitDiffResponse
+          init_from_gitaly(raw_diff)
+          prune_diff_if_eligible(collapse)
         when nil
           raise "Nil as raw diff passed"
         else
@@ -266,13 +270,26 @@ module Gitlab
         @diff = encode!(strip_diff_headers(patch.to_s))
       end
 
-      def init_from_hash(hash, collapse: false)
+      def init_from_hash(hash)
         raw_diff = hash.symbolize_keys
 
         serialize_keys.each do |key|
           send(:"#{key}=", raw_diff[key.to_sym])
         end
+      end
+
+      def init_from_gitaly(diff_msg)
+        @diff = diff_msg.raw_chunks.join
+        @new_path = encode!(diff_msg.to_path.dup)
+        @old_path = encode!(diff_msg.from_path.dup)
+        @a_mode = diff_msg.old_mode.to_s(8)
+        @b_mode = diff_msg.new_mode.to_s(8)
+        @new_file = diff_msg.from_id == BLANK_SHA
+        @renamed_file = diff_msg.from_path != diff_msg.to_path
+        @deleted_file = diff_msg.to_id == BLANK_SHA
+      end
 
+      def prune_diff_if_eligible(collapse = false)
         prune_large_diff! if too_large?
         prune_collapsed_diff! if collapse && collapsible?
       end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065dbe83ab9b14be856b0d4b64183c071..4e45ec7c17407ebd99b0733154b4720276972db1 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
         elsif @deltas_only
           each_delta(&block)
         else
-          each_patch(&block)
+          Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+            each_patch(&block)
+          end
         end
       end
 
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af1744c9c463aac33904245ad3aa5b5ae333a327
--- /dev/null
+++ b/lib/gitlab/git/index.rb
@@ -0,0 +1,126 @@
+module Gitlab
+  module Git
+    class Index
+      DEFAULT_MODE = 0o100644
+
+      attr_reader :repository, :raw_index
+
+      def initialize(repository)
+        @repository = repository
+        @raw_index = repository.rugged.index
+      end
+
+      delegate :read_tree, :get, to: :raw_index
+
+      def write_tree
+        raw_index.write_tree(repository.rugged)
+      end
+
+      def dir_exists?(path)
+        raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
+      end
+
+      def create(options)
+        options = normalize_options(options)
+
+        file_entry = get(options[:file_path])
+        if file_entry
+          raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
+        end
+
+        add_blob(options)
+      end
+
+      def create_dir(options)
+        options = normalize_options(options)
+
+        file_entry = get(options[:file_path])
+        if file_entry
+          raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
+        end
+
+        if dir_exists?(options[:file_path])
+          raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
+        end
+
+        options = options.dup
+        options[:file_path] += '/.gitkeep'
+        options[:content] = ''
+
+        add_blob(options)
+      end
+
+      def update(options)
+        options = normalize_options(options)
+
+        file_entry = get(options[:file_path])
+        unless file_entry
+          raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+        end
+
+        add_blob(options, mode: file_entry[:mode])
+      end
+
+      def move(options)
+        options = normalize_options(options)
+
+        file_entry = get(options[:previous_path])
+        unless file_entry
+          raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+        end
+
+        raw_index.remove(options[:previous_path])
+
+        add_blob(options, mode: file_entry[:mode])
+      end
+
+      def delete(options)
+        options = normalize_options(options)
+
+        file_entry = get(options[:file_path])
+        unless file_entry
+          raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+        end
+
+        raw_index.remove(options[:file_path])
+      end
+
+      private
+
+      def normalize_options(options)
+        options = options.dup
+        options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
+        options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
+        options
+      end
+
+      def normalize_path(path)
+        pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
+
+        if pathname.each_filename.include?('..')
+          raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+        end
+
+        pathname.to_s
+      end
+
+      def add_blob(options, mode: nil)
+        content = options[:content]
+        content = Base64.decode64(content) if options[:encoding] == 'base64'
+
+        detect = CharlockHolmes::EncodingDetector.new.detect(content)
+        unless detect && detect[:type] == :binary
+          # When writing to the repo directly as we are doing here,
+          # the `core.autocrlf` config isn't taken into account.
+          content.gsub!("\r\n", "\n") if repository.autocrlf
+        end
+
+        oid = repository.rugged.write(content, :blob)
+
+        raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
+      rescue Rugged::IndexError => e
+        raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 7068e68a8553df04f91b59cbb9053e0cc8b40463..2187dd70ff4a60543e2e050ee38aa14401ab00e9 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,5 +1,4 @@
 # Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
-require 'forwardable'
 require 'tempfile'
 require 'forwardable'
 require "rubygems/package"
@@ -7,14 +6,13 @@ require "rubygems/package"
 module Gitlab
   module Git
     class Repository
-      extend Forwardable
       include Gitlab::Git::Popen
 
       SEARCH_CONTEXT_LINES = 3
 
-      class NoRepository < StandardError; end
-      class InvalidBlobName < StandardError; end
-      class InvalidRef < StandardError; end
+      NoRepository = Class.new(StandardError)
+      InvalidBlobName = Class.new(StandardError)
+      InvalidRef = Class.new(StandardError)
 
       # Full path to repo
       attr_reader :path
@@ -33,6 +31,10 @@ module Gitlab
         @attributes = Gitlab::Git::Attributes.new(path)
       end
 
+      delegate  :empty?,
+                :bare?,
+                to: :rugged
+
       # Default branch in the repository
       def root_ref
         @root_ref ||= discover_default_branch
@@ -162,14 +164,6 @@ module Gitlab
         !empty?
       end
 
-      def empty?
-        rugged.empty?
-      end
-
-      def bare?
-        rugged.bare?
-      end
-
       def repo_exists?
         !!rugged
       end
@@ -205,13 +199,17 @@ module Gitlab
         nil
       end
 
+      def archive_prefix(ref, sha)
+        project_name = self.name.chomp('.git')
+        "#{project_name}-#{ref.tr('/', '-')}-#{sha}"
+      end
+
       def archive_metadata(ref, storage_path, format = "tar.gz")
         ref ||= root_ref
         commit = Gitlab::Git::Commit.find(self, ref)
         return {} if commit.nil?
 
-        project_name = self.name.chomp('.git')
-        prefix = "#{project_name}-#{ref}-#{commit.id}"
+        prefix = archive_prefix(ref, commit.id)
 
         {
           'RepoPath' => path,
@@ -330,24 +328,42 @@ module Gitlab
       end
 
       def log_by_shell(sha, options)
-        cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
-        cmd += %W(-n #{options[:limit].to_i})
-        cmd += %w(--format=%H)
-        cmd += %W(--skip=#{options[:offset].to_i})
-        cmd += %w(--follow) if options[:follow]
-        cmd += %w(--no-merges) if options[:skip_merges]
-        cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
-        cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
-        cmd += [sha]
-        cmd += %W(-- #{options[:path]}) if options[:path].present?
-
-        raw_output = IO.popen(cmd) {|io| io.read }
-
-        log = raw_output.lines.map do |c|
-          Rugged::Commit.new(rugged, c.strip)
-        end
+        limit = options[:limit].to_i
+        offset = options[:offset].to_i
+        use_follow_flag = options[:follow] && options[:path].present?
+
+        # We will perform the offset in Ruby because --follow doesn't play well with --skip.
+        # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+        offset_in_ruby = use_follow_flag && options[:offset].present?
+        limit += offset if offset_in_ruby
+
+        cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
+        cmd << "--max-count=#{limit}"
+        cmd << '--format=%H'
+        cmd << "--skip=#{offset}" unless offset_in_ruby
+        cmd << '--follow' if use_follow_flag
+        cmd << '--no-merges' if options[:skip_merges]
+        cmd << "--after=#{options[:after].iso8601}" if options[:after]
+        cmd << "--before=#{options[:before].iso8601}" if options[:before]
+        cmd << sha
+        cmd += %W[-- #{options[:path]}] if options[:path].present?
 
-        log.is_a?(Array) ? log : []
+        raw_output = IO.popen(cmd) { |io| io.read }
+        lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
+
+        lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
+      end
+
+      def count_commits(options)
+        cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
+        cmd << "--after=#{options[:after].iso8601}" if options[:after]
+        cmd << "--before=#{options[:before].iso8601}" if options[:before]
+        cmd += %W[--count #{options[:ref]}]
+        cmd += %W[-- #{options[:path]}] if options[:path].present?
+
+        raw_output = IO.popen(cmd) { |io| io.read }
+
+        raw_output.to_i
       end
 
       def sha_from_ref(ref)
@@ -565,9 +581,7 @@ module Gitlab
       #    will trigger a +:mixed+ reset and the working directory will be
       #    replaced with the content of the index. (Untracked and ignored files
       #    will be left alone)
-      def reset(ref, reset_type)
-        rugged.reset(ref, reset_type)
-      end
+      delegate :reset, to: :rugged
 
       # Mimic the `git clean` command and recursively delete untracked files.
       # Valid keys that can be passed in the +options+ hash are:
@@ -845,57 +859,6 @@ module Gitlab
         rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
       end
 
-      # Create a new directory with a .gitkeep file. Creates
-      # all required nested directories (i.e. mkdir -p behavior)
-      #
-      # options should contain next structure:
-      #   author: {
-      #     email: 'user@example.com',
-      #     name: 'Test User',
-      #     time: Time.now
-      #   },
-      #   committer: {
-      #     email: 'user@example.com',
-      #     name: 'Test User',
-      #     time: Time.now
-      #   },
-      #   commit: {
-      #     message: 'Wow such commit',
-      #     branch: 'master',
-      #     update_ref: false
-      #   }
-      def mkdir(path, options = {})
-        # Check if this directory exists; if it does, then don't bother
-        # adding .gitkeep file.
-        ref = options[:commit][:branch]
-        path = Gitlab::Git::PathHelper.normalize_path(path).to_s
-        rugged_ref = rugged.ref(ref)
-
-        raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
-
-        target_commit = rugged_ref.target
-
-        raise InvalidRef.new("Invalid target commit") if target_commit.nil?
-
-        entry = tree_entry(target_commit, path)
-
-        if entry
-          if entry[:type] == :blob
-            raise InvalidBlobName.new("Directory already exists as a file")
-          else
-            raise InvalidBlobName.new("Directory already exists")
-          end
-        end
-
-        options[:file] = {
-          content: '',
-          path: "#{path}/.gitkeep",
-          update: true
-        }
-
-        Gitlab::Git::Blob.commit(self, options)
-      end
-
       # Returns result like "git ls-files" , recursive and full file path
       #
       # Ex.
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7e1484613f2895f6be896eec1e487d83a8aaf7fc..eea2f206902f2bf855a607ebd0e028bcf07ce3a4 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -10,10 +10,10 @@ module Gitlab
       deploy_key_upload:
         'This deploy key does not have write access to this project.',
       no_repo: 'A repository for this project does not exist yet.'
-    }
+    }.freeze
 
-    DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
-    PUSH_COMMANDS = %w{ git-receive-pack }
+    DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
+    PUSH_COMMANDS = %w{ git-receive-pack }.freeze
     ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
 
     attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
@@ -153,7 +153,9 @@ module Gitlab
         user_access: user_access,
         project: project,
         env: @env,
-        skip_authorization: deploy_key?).exec
+        skip_authorization: deploy_key?,
+        protocol: protocol
+      ).exec
     end
 
     def matching_merge_request?(newrev, branch_name)
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 4d83d8e72a8b3674c3fdb30e689a3b05f5c5b277..0e87ee30c985d7bf225674475fe31812bd77fae9 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,6 +5,9 @@ module Gitlab
     #
     # Returns true for a valid reference name, false otherwise
     def validate(ref_name)
+      return false if ref_name.start_with?('refs/heads/')
+      return false if ref_name.start_with?('refs/remotes/')
+
       Gitlab::Utils.system_silent(
         %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
     end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5534d4af4397b58ac0c23cf7b1640620c47fe43b
--- /dev/null
+++ b/lib/gitlab/gitaly_client.rb
@@ -0,0 +1,43 @@
+require 'gitaly'
+
+module Gitlab
+  module GitalyClient
+    def self.gitaly_address
+      if Gitlab.config.gitaly.socket_path
+        "unix://#{Gitlab.config.gitaly.socket_path}"
+      end
+    end
+
+    def self.channel
+      return @channel if defined?(@channel)
+
+      @channel =
+        if enabled?
+          # NOTE: Gitaly currently runs on a Unix socket, so permissions are
+          # handled using the file system and no additional authentication is
+          # required (therefore the :this_channel_is_insecure flag)
+          GRPC::Core::Channel.new(gitaly_address, {}, :this_channel_is_insecure)
+        else
+          nil
+        end
+    end
+
+    def self.enabled?
+      gitaly_address.present?
+    end
+
+    def self.feature_enabled?(feature)
+      enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+    end
+
+    def self.migrate(feature)
+      is_enabled  = feature_enabled?(feature)
+      metric_name = feature.to_s
+      metric_name += "_gitaly" if is_enabled
+
+      Gitlab::Metrics.measure(metric_name) do
+        yield is_enabled
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 0000000000000000000000000000000000000000..525b8d680e90fc0f2b00db1191b850a0fb207bba
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,25 @@
+module Gitlab
+  module GitalyClient
+    class Commit
+      # The ID of empty tree.
+      # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+      EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+      class << self
+        def diff_from_parent(commit, options = {})
+          stub      = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+          repo      = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+          parent    = commit.parents[0]
+          parent_id = parent ? parent.id : EMPTY_TREE_ID
+          request   = Gitaly::CommitDiffRequest.new(
+            repository: repo,
+            left_commit_id: parent_id,
+            right_commit_id: commit.id
+          )
+
+          Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b827a56207f6bb5e9b32704b39c4a71fbbe28f6e
--- /dev/null
+++ b/lib/gitlab/gitaly_client/notifications.rb
@@ -0,0 +1,17 @@
+module Gitlab
+  module GitalyClient
+    class Notifications
+      attr_accessor :stub
+
+      def initialize
+        @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+      end
+
+      def post_receive(repo_path)
+        repository = Gitaly::Repository.new(path: repo_path)
+        request = Gitaly::PostReceiveRequest.new(repository: repository)
+        stub.post_receive(request)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 0a8d05b5fe1d327d672ee7e1d7d9531aa1e9a687..5d29e698b2728590613a5bbe1222929a828bfb11 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -18,7 +18,7 @@ module Gitlab
       end
 
       def commit_exists?
-        project.repository.commit(sha).present?
+        project.repository.branch_names_contains(sha).include?(ref)
       end
 
       def short_id
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index d95ff4fd104ffd823b6a45cb022ca9e88f6ea371..eea4a91f17d4f8a8e8d3fcba3dfe724fd1f74e72 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -171,6 +171,8 @@ module Gitlab
       end
 
       def clean_up_restored_branches(pull_request)
+        return if pull_request.opened?
+
         remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
         remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
       end
@@ -285,7 +287,7 @@ module Gitlab
       def fetch_resources(resource_type, *opts)
         return if imported?(resource_type)
 
-        opts.last.merge!(page: current_page(resource_type))
+        opts.last[:page] = current_page(resource_type)
 
         client.public_send(resource_type, *opts) do |resources|
           yield resources
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
index 29fb0f9d3337f832397917c1cf92ef65a7ce64c6..27b171d6ddb4bfaad84b681d7730e891ed46b8af 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -7,9 +7,7 @@ module Gitlab
         raise NotImplementedError
       end
 
-      def number
-        raw_data.number
-      end
+      delegate :number, to: :raw_data
 
       def find_condition
         { iid: number }
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 4ea0200e89b7f928e25ae91c84a7b93bc50b887e..add7236e3398362b4d8db63c3f4b775083a4214c 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -38,7 +38,11 @@ module Gitlab
 
       def source_branch_name
         @source_branch_name ||= begin
-          source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+          if cross_project?
+            "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}"
+          else
+            source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+          end
         end
       end
 
@@ -52,6 +56,14 @@ module Gitlab
         end
       end
 
+      def cross_project?
+        source_branch.repo.id != target_branch.repo.id
+      end
+
+      def opened?
+        state == 'opened'
+      end
+
       private
 
       def state
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index b8a5ac907a44f9356d385fa7ed88e661e3235afa..5ab84266b7d5176a4500c52c1966f73e5469aa2b 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -1,19 +1,20 @@
 module Gitlab
   module GonHelper
     def add_gon_variables
-      gon.api_version            = API::API.version
-      gon.default_avatar_url     = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+      gon.api_version            = 'v4'
+      gon.default_avatar_url     = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
       gon.max_file_size          = current_application_settings.max_attachment_size
+      gon.asset_host             = ActionController::Base.asset_host
       gon.relative_url_root      = Gitlab.config.gitlab.relative_url_root
       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
         gon.current_username = current_user.username
+        gon.current_user_fullname = current_user.name
       end
     end
   end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 9360afedfcb4b1df697f97482d8aede813a4dac5..d787d5db4a0bd80013097d1dbe08d5d9eeedebfc 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -14,7 +14,7 @@ module Gitlab
     end
 
     def initialize(blob_name, blob_content, repository: nil)
-      @formatter = Rouge::Formatters::HTMLGitlab.new
+      @formatter = Rouge::Formatters::HTMLGitlab
       @repository = repository
       @blob_name = blob_name
       @blob_content = blob_content
@@ -28,7 +28,7 @@ module Gitlab
         hl_lexer = self.lexer
       end
 
-      @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe
+      @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
     rescue
       @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
     end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index a46a41bc56ea7bb839b5bb984acfc784876974d1..8b327cfc226171d207bcbdb1ba3f55bc244879bd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
     extend self
 
     # For every version update, the version history in import_export.md has to be kept up to date.
-    VERSION = '0.1.6'
+    VERSION = '0.1.6'.freeze
     FILENAME_LIMIT = 50
 
     def export_path(relative_path:)
@@ -35,7 +35,7 @@ module Gitlab
     end
 
     def export_filename(project:)
-      basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.full_path}_#{project.path}"
+      basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
 
       "#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
     end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index e341c4d9cf898973e9cf3a63a29a11ed3b562750..788eedf2686d407e3a1294ad9ffc74e08163eead 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -1,5 +1,5 @@
 module Gitlab
   module ImportExport
-    class Error < StandardError; end
+    Error = Class.new(StandardError)
   end
 end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 416194e57d7fd7a4dc26127c25edb1acc6ee98ac..ab74c8782f6f08911aa8a07d2429c8039b97551a 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,9 @@ excluded_attributes:
     - :milestone_id
   award_emoji:
     - :awardable_id
+  statuses:
+    - :trace
+    - :token
 
 methods:
   labels:
@@ -81,6 +84,7 @@ methods:
     - :type
   statuses:
     - :type
+    - :gl_project_id
   services:
     - :type
   merge_request_diff:
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index b79be62245b50825884c613e29be744382d5ab3a..3473b46693612c3c85f80720d55c9bdace81e2a9 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -47,7 +47,13 @@ module Gitlab
       def group_members
         return [] unless @current_user.can?(:admin_group, @project.group)
 
-        MembersFinder.new(@project.project_members, @project.group).execute(@current_user)
+        # We need `.where.not(user_id: nil)` here otherwise when a group has an
+        # invitee, it would make the following query return 0 rows since a NULL
+        # user_id would be present in the subquery
+        # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+        non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
+
+        GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
       end
     end
   end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index fae792237d90370d0a9a5cb1e62f8002bd57b400..d44563333a5032f29122c9e76e3aedf1105c8355 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -15,7 +15,7 @@ module Gitlab
 
       USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
 
-      PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+      PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
 
       BUILD_MODELS = %w[Ci::Build commit_status].freeze
 
@@ -98,12 +98,11 @@ module Gitlab
       end
 
       def generate_imported_object
-        if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
-          trace = @relation_hash.delete('trace')
+        if BUILD_MODELS.include?(@relation_name)
+          @relation_hash.delete('trace') # old export files have trace
           @relation_hash.delete('token')
 
           imported_object do |object|
-            object.trace = trace
             object.commit_id = nil
           end
         else
@@ -121,7 +120,6 @@ module Gitlab
 
         # project_id may not be part of the export, but we always need to populate it if required.
         @relation_hash['project_id'] = project_id
-        @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
         @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
       end
 
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 7084fd1767dceaee83abb012a440c212181e3624..43eb73250b770559e045a801106c1fc0d931baa8 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -43,9 +43,7 @@ module Gitlab
         attribute_value(:email)
       end
 
-      def dn
-        entry.dn
-      end
+      delegate :dn, to: :entry
 
       private
 
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index b84c81f1a6cd098954e2f7fc126b1f34b317191f..2d5e47a6f3b7e648b0ae9bdddaa0327463066b52 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -1,5 +1,3 @@
-require 'gitlab/o_auth/user'
-
 # LDAP extension for User model
 #
 # * Find or create user from omniauth.auth data
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 4b7a791e497245ae3b801ca9c2c77339c27559c9..6aa38542cb4b1dd1058a9314a081d7f08791bd0a 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -143,11 +143,12 @@ module Gitlab
         # signature this would break things. As a result we'll make sure the
         # generated method _only_ accepts regular arguments if the underlying
         # method also accepts them.
-        if method.arity == 0
-          args_signature = ''
-        else
-          args_signature = '*args'
-        end
+        args_signature =
+          if method.arity == 0
+            ''
+          else
+            '*args'
+          end
 
         proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
           def #{name}(#{args_signature})
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 47f88727fc8d6c3aae1db6d2ddfd6d3822f66172..adc0db1a8743ba9c193bd87cb5c33008e0188895 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,8 +2,8 @@ module Gitlab
   module Metrics
     # Rack middleware for tracking Rails and Grape requests.
     class RackMiddleware
-      CONTROLLER_KEY = 'action_controller.instance'
-      ENDPOINT_KEY   = 'api.endpoint'
+      CONTROLLER_KEY = 'action_controller.instance'.freeze
+      ENDPOINT_KEY   = 'api.endpoint'.freeze
       CONTENT_TYPES = {
         'text/html' => :html,
         'text/plain' => :txt,
@@ -14,7 +14,7 @@ module Gitlab
         'image/jpeg' => :jpeg,
         'image/gif' => :gif,
         'image/svg+xml' => :svg
-      }
+      }.freeze
 
       def initialize(app)
         @app = app
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 2e9dd4645e39d84719a7f4be0123d179168b635b..d435a33e9c7c5fd9667ca829334349865b446e66 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -5,7 +5,7 @@ module Gitlab
       class ActionView < ActiveSupport::Subscriber
         attach_to :action_view
 
-        SERIES = 'views'
+        SERIES = 'views'.freeze
 
         def render_template(event)
           track(event) if current_transaction
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 7bc16181be624cac25ed051d8b27b94a20a04440..4f9fb1c78537827b850a3c21eb085809ecf43381 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -5,7 +5,7 @@ module Gitlab
       THREAD_KEY = :_gitlab_metrics_transaction
 
       # The series to store events (e.g. Git pushes) in.
-      EVENT_SERIES = 'events'
+      EVENT_SERIES = 'events'.freeze
 
       attr_reader :tags, :values, :method, :metrics
 
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 5764ab1565279617829350145d1b020286745955..6023fa1820fc69ca44821d1576fdd2632e3b6e58 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -30,21 +30,69 @@ module Gitlab
       end
 
       def go_body(request)
-        base_url = Gitlab.config.gitlab.url
-        # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
-        # We can just ignore the paths and leave the namespace/project
-        path_info = request.env["PATH_INFO"]
-        path_info.sub!(/^\//, '')
-        project_path = path_info.split('/').first(2).join('/')
-        request_url = URI.join(base_url, project_path)
-        domain_path = strip_url(request_url.to_s)
+        project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+        import_prefix = strip_url(project_url.to_s)
 
-        "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"
+        "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
       end
 
       def strip_url(url)
         url.gsub(/\Ahttps?:\/\//, '')
       end
+
+      def project_path(request)
+        path_info = request.env["PATH_INFO"]
+        path_info.sub!(/^\//, '')
+
+        # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
+        # In a traditional project with a single namespace, this would denote repo
+        # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
+        # groups, this could also be `namespace/project/path1` with subpath
+        # `path2/../pathN`, for example.
+
+        # We find all potential project paths out of the path segments
+        path_segments = path_info.split('/')
+        simple_project_path = path_segments.first(2).join('/')
+
+        # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
+        return simple_project_path if path_segments.length <= 2
+
+        project_paths = []
+        begin
+          project_paths << path_segments.join('/')
+          path_segments.pop
+        end while path_segments.length >= 2
+
+        # We see if a project exists with any of these potential paths
+        project = project_for_paths(project_paths, request)
+
+        if project
+          # If a project is found and the user has access, we return the full project path
+          project.full_path
+        else
+          # If not, we return the first two components as if it were a simple `namespace/project` path,
+          # so that we don't reveal the existence of a nested project the user doesn't have access to.
+          # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
+          # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
+          # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
+          # Since `go get` doesn't authenticate by default, this means that
+          # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
+          # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
+          # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
+          simple_project_path
+        end
+      end
+
+      def project_for_paths(paths, request)
+        project = Project.where_full_path_in(paths).first
+        return unless Ability.allowed?(current_user(request), :read_project, project)
+
+        project
+      end
+
+      def current_user(request)
+        request.env['warden']&.authenticate
+      end
     end
   end
 end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index dd99f9bb7d76293abe156f9e7a99a9103c976a22..fee741b47be91bc53f1b2bf8f444ac83650422af 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -26,7 +26,7 @@
 module Gitlab
   module Middleware
     class Multipart
-      RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+      RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'.freeze
 
       class Handler
         def initialize(env, message)
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
index 3fe32adeade7bcdc8282a1c7ebeea00be7c5d34e..6105d16581040b7a7c864d9511f10838d8016894 100644
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -8,16 +8,16 @@ module Gitlab
         @proxy_host = opts.fetch(:proxy_host, 'localhost')
         @proxy_port = opts.fetch(:proxy_port, 3808)
         @proxy_path = opts[:proxy_path] if opts[:proxy_path]
-        super(app, opts)
+
+        super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
       end
 
       def perform_request(env)
-        unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
-          return @app.call(env)
+        if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+          super(env)
+        else
+          @app.call(env)
         end
-
-        env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
-        super(env)
       end
     end
   end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 96ed20af9189982598973cccf44fa054250bae97..fcf51b7fc5bfc157bb7884c73a4ae79e1771ea9f 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -5,7 +5,7 @@
 #
 module Gitlab
   module OAuth
-    class SignupDisabledError < StandardError; end
+    SignupDisabledError = Class.new(StandardError)
 
     class User
       attr_accessor :auth_hash, :gl_user
@@ -29,12 +29,11 @@ module Gitlab
       def save(provider = 'OAuth')
         unauthorized_to_create unless gl_user
 
-        if needs_blocking?
-          gl_user.save!
-          gl_user.block
-        else
-          gl_user.save!
-        end
+        block_after_save = needs_blocking?
+
+        gl_user.save!
+
+        gl_user.block if block_after_save
 
         log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
         gl_user
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 879d46446b398e83eb0ea8374970e97cd3313562..962ff4d39850139020d53a6f6ae2e5c1381005bf 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -1,12 +1,12 @@
 module Gitlab
   module OptimisticLocking
-    extend self
+    module_function
 
     def retry_lock(subject, retries = 100, &block)
       loop do
         begin
           ActiveRecord::Base.transaction do
-            return block.call(subject)
+            return yield(subject)
           end
         rescue ActiveRecord::StaleObjectError
           retries -= 1
@@ -15,5 +15,7 @@ module Gitlab
         end
       end
     end
+
+    alias_method :retry_optimistic_lock, :retry_lock
   end
 end
diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62239779454f692db905e5fac388ae0d87fc69b8
--- /dev/null
+++ b/lib/gitlab/prometheus.rb
@@ -0,0 +1,70 @@
+module Gitlab
+  PrometheusError = Class.new(StandardError)
+
+  # Helper methods to interact with Prometheus network services & resources
+  class Prometheus
+    attr_reader :api_url
+
+    def initialize(api_url:)
+      @api_url = api_url
+    end
+
+    def ping
+      json_api_get('query', query: '1')
+    end
+
+    def query(query)
+      get_result('vector') do
+        json_api_get('query', query: query)
+      end
+    end
+
+    def query_range(query, start: 8.hours.ago)
+      get_result('matrix') do
+        json_api_get('query_range',
+          query: query,
+          start: start.to_f,
+          end: Time.now.utc.to_f,
+          step: 1.minute.to_i)
+      end
+    end
+
+    private
+
+    def json_api_get(type, args = {})
+      get(join_api_url(type, args))
+    rescue Errno::ECONNREFUSED
+      raise PrometheusError, 'Connection refused'
+    end
+
+    def join_api_url(type, args = {})
+      url = URI.parse(api_url)
+    rescue URI::Error
+      raise PrometheusError, "Invalid API URL: #{api_url}"
+    else
+      url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
+      url.query = args.to_query
+
+      url.to_s
+    end
+
+    def get(url)
+      handle_response(HTTParty.get(url))
+    end
+
+    def handle_response(response)
+      if response.code == 200 && response['status'] == 'success'
+        response['data'] || {}
+      elsif response.code == 400
+        raise PrometheusError, response['error'] || 'Bad data received'
+      else
+        raise PrometheusError, "#{response.code} - #{response.body}"
+      end
+    end
+
+    def get_result(expected_type)
+      data = yield
+      data['result'] if data['resultType'] == expected_type
+    end
+  end
+end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9384102acec65adc0ce3bd3bfe408363f0422d28..bc5370de32aa5fd8f1ed126c99a2be1b40ff7855 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,27 +1,18 @@
 # This file should not have any direct dependency on Rails environment
 # please require all dependencies below:
 require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/module/delegation'
 
 module Gitlab
   class Redis
-    CACHE_NAMESPACE = 'cache:gitlab'
-    SESSION_NAMESPACE = 'session:gitlab'
-    SIDEKIQ_NAMESPACE = 'resque:gitlab'
-    MAILROOM_NAMESPACE = 'mail_room:gitlab'
-    DEFAULT_REDIS_URL = 'redis://localhost:6379'
-    CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
+    CACHE_NAMESPACE = 'cache:gitlab'.freeze
+    SESSION_NAMESPACE = 'session:gitlab'.freeze
+    SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
+    MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
+    DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
 
     class << self
-      # Do NOT cache in an instance variable. Result may be mutated by caller.
-      def params
-        new.params
-      end
-
-      # Do NOT cache in an instance variable. Result may be mutated by caller.
-      # @deprecated Use .params instead to get sentinel support
-      def url
-        new.url
-      end
+      delegate :params, :url, to: :new
 
       def with
         @pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@@ -42,13 +33,17 @@ module Gitlab
         return @_raw_config if defined?(@_raw_config)
 
         begin
-          @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
+          @_raw_config = ERB.new(File.read(config_file)).result.freeze
         rescue Errno::ENOENT
           @_raw_config = false
         end
 
         @_raw_config
       end
+
+      def config_file
+        ENV['GITLAB_REDIS_CONFIG_FILE'] || File.expand_path('../../config/resque.yml', __dir__)
+      end
     end
 
     def initialize(rails_env = nil)
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 437a339dd2b2965b995506ec6d8665f87726781a..7668ecacc4b7e2d4c7dcee5e76fcfdf28b47649d 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,7 +1,7 @@
 module Gitlab
   # Extract possible GFM references from an arbitrary String for further processing.
   class ReferenceExtractor < Banzai::ReferenceExtractor
-    REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user)
+    REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze
     attr_accessor :project, :current_user, :author
 
     def initialize(project, current_user = nil)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index c77fe2d8bdc586b103f91381f1c66bc71fe09f9c..5e5f5ff1589b319aba3dc2f1bc8ec3620973706f 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,17 +5,18 @@ module Gitlab
     # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
     # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
     # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
-    # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+    # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
     # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
     # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
     PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
-    NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
-    NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
-    PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze
+    NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+    NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
+    NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
+    PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
 
     # Same as NAMESPACE_REGEX_STR but allows `/` in the path.
     # So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
-    NAMESPACE_REF_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.\/]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
+    FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
 
     def namespace_regex
       @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fef536ecb0bf27092a4c05315017cd37c8e7d469
--- /dev/null
+++ b/lib/gitlab/request_context.rb
@@ -0,0 +1,21 @@
+module Gitlab
+  class RequestContext
+    class << self
+      def client_ip
+        RequestStore[:client_ip]
+      end
+    end
+
+    def initialize(app)
+      @app = app
+    end
+
+    def call(env)
+      req = Rack::Request.new(env)
+
+      RequestStore[:client_ip] = req.ip
+
+      @app.call(env)
+    end
+  end
+end
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index 8130e55351e2b5470669bcce845e8a3ccf96e428..0c9ab759e81b0a4490ea3479945b5551640e2fcf 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -2,7 +2,7 @@ require 'fileutils'
 
 module Gitlab
   module RequestProfiler
-    PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
+    PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles".freeze
 
     def profile_token
       Rails.cache.fetch('profile-token') do
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
index 72d00abfcc26e2995bc9fdb9ac56ff572f0acd98..36791fae60f9c70e7d7443090ad84ed10b909331 100644
--- a/lib/gitlab/route_map.rb
+++ b/lib/gitlab/route_map.rb
@@ -1,6 +1,6 @@
 module Gitlab
   class RouteMap
-    class FormatError < StandardError; end
+    FormatError = Class.new(StandardError)
 
     def initialize(data)
       begin
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index f253dc7477e7a31a2c6af33c2f9c38337dfd1843..8a7cc69004641c54978732ae78edcf9494ec05af 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,11 +28,12 @@ module Gitlab
         if external_users_enabled? && @user
           # Check if there is overlap between the user's groups and the external groups
           # setting then set user as external or internal.
-          if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
-            @user.external = false
-          else
-            @user.external = true
-          end
+          @user.external =
+            if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+              false
+            else
+              true
+            end
         end
 
         @user
diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb
index 7b6b70d8dbce79a00713c3e5030e26046dc787bc..d50f826f92407ce1045dfe9dceab8d3565f98865 100644
--- a/lib/gitlab/sanitizers/svg/whitelist.rb
+++ b/lib/gitlab/sanitizers/svg/whitelist.rb
@@ -6,18 +6,19 @@ module Gitlab
     module SVG
       class Whitelist
         ALLOWED_ELEMENTS = %w[
-        a altGlyph altGlyphDef altGlyphItem animate
-        animateColor animateMotion animateTransform circle clipPath color-profile
-        cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
-        feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
-        feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
-        feImage feMerge feMergeNode feMorphology feOffset fePointLight
-        feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
-        font-face-format font-face-name font-face-src font-face-uri foreignObject
-        g glyph glyphRef hkern image line linearGradient marker mask metadata
-        missing-glyph mpath path pattern polygon polyline radialGradient rect
-        script set stop style svg switch symbol text textPath title tref tspan use
-        view vkern].freeze
+          a altGlyph altGlyphDef altGlyphItem animate
+          animateColor animateMotion animateTransform circle clipPath color-profile
+          cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
+          feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
+          feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
+          feImage feMerge feMergeNode feMorphology feOffset fePointLight
+          feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
+          font-face-format font-face-name font-face-src font-face-uri foreignObject
+          g glyph glyphRef hkern image line linearGradient marker mask metadata
+          missing-glyph mpath path pattern polygon polyline radialGradient rect
+          script set stop style svg switch symbol text textPath title tref tspan use
+          view vkern
+        ].freeze
 
         ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
 
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index c9c65f76f4bb263724dab2d7736160b921f3fd9b..ccfa517e04b5f2be20ec2d5f60ed5dcc3bab206a 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -56,11 +56,12 @@ module Gitlab
     def issues
       issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
 
-      if query =~ /#(\d+)\z/
-        issues = issues.where(iid: $1)
-      else
-        issues = issues.full_search(query)
-      end
+      issues =
+        if query =~ /#(\d+)\z/
+          issues.where(iid: $1)
+        else
+          issues.full_search(query)
+        end
 
       issues.order('updated_at DESC')
     end
@@ -73,11 +74,12 @@ module Gitlab
 
     def merge_requests
       merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
-      if query =~ /[#!](\d+)\z/
-        merge_requests = merge_requests.where(iid: $1)
-      else
-        merge_requests = merge_requests.full_search(query)
-      end
+      merge_requests =
+        if query =~ /[#!](\d+)\z/
+          merge_requests.where(iid: $1)
+        else
+          merge_requests.full_search(query)
+        end
       merge_requests.order('updated_at DESC')
     end
 
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 7cf506ebe640d6c7f63978689c03ae55ac8557f7..823f697f51cfa9a0a86067ae3b3b5edef31c9206 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -1,24 +1,23 @@
+module DeliverNever
+  def deliver_later
+    self
+  end
+end
+
 module Gitlab
   class Seeder
     def self.quiet
       mute_mailer
       SeedFu.quiet = true
+
       yield
+
       SeedFu.quiet = false
       puts "\nOK".color(:green)
     end
 
-    def self.by_user(user)
-      yield
-    end
-
     def self.mute_mailer
-      code = <<-eos
-def Notify.deliver_later
-  self
-end
-      eos
-      eval(code)
+      ActionMailer::MessageDelivery.prepend(DeliverNever)
     end
   end
 end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index bf2c0acc7298420e9d1617449c602027ffeccebd..9c92b83dddcfb5609f9b726fccb250bf7d684284 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -1,7 +1,7 @@
 module Gitlab
   module Serializer
     class Pagination
-      class InvalidResourceError < StandardError; end
+      InvalidResourceError = Class.new(StandardError)
       include ::API::Helpers::Pagination
 
       def initialize(request, response)
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 3faa336f142112d91b5f91999044e855d622148a..da8d8ddb8edf7206fbd3e21cc148869db428a606 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -2,7 +2,7 @@ require 'securerandom'
 
 module Gitlab
   class Shell
-    class Error < StandardError; end
+    Error = Class.new(StandardError)
 
     KeyAdder = Struct.new(:io) do
       def add_key(id, key)
@@ -82,8 +82,8 @@ module Gitlab
     def import_repository(storage, name, url)
       # Timeout should be less than 900 ideally, to prevent the memory killer
       # to silently kill the process without knowing we are timing out here.
-      output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
-                                     storage, "#{name}.git", url, '800'])
+      output, status = Popen.popen([gitlab_shell_projects_path, 'import-project',
+                                    storage, "#{name}.git", url, '800'])
       raise Error, output unless status.zero?
       true
     end
@@ -145,7 +145,7 @@ module Gitlab
     #   batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") }
     def batch_add_keys(&block)
       IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io|
-        block.call(KeyAdder.new(io))
+        yield(KeyAdder.new(io))
       end
     end
 
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 4917c4ae2ac5c1a0a3b95b72d8cd2a25d42709db..99e56e923eb08a191d3583e2baf5ab815e6d2d58 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -94,11 +94,12 @@ module Gitlab
       private
 
       def raw_explain(query)
-        if Gitlab::Database.postgresql?
-          explain = "EXPLAIN ANALYZE #{query};"
-        else
-          explain = "EXPLAIN #{query};"
-        end
+        explain =
+          if Gitlab::Database.postgresql?
+            "EXPLAIN ANALYZE #{query};"
+          else
+            "EXPLAIN #{query};"
+          end
 
         ActiveRecord::Base.connection.execute(explain)
       end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index aadc401ff8d3f7288efa1ae6617d02495b6238fb..11e5f1b645c18a7b08fc9f7610c2bc1e10911b34 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -44,19 +44,42 @@ module Gitlab
 
     # Returns true if all the given job have been completed.
     #
-    # jids - The Sidekiq job IDs to check.
+    # job_ids - The Sidekiq job IDs to check.
     #
     # Returns true or false.
-    def self.all_completed?(jids)
-      keys = jids.map { |jid| key_for(jid) }
+    def self.all_completed?(job_ids)
+      self.num_running(job_ids).zero?
+    end
+
+    # Returns the number of jobs that are running.
+    #
+    # job_ids - The Sidekiq job IDs to check.
+    def self.num_running(job_ids)
+      responses = self.job_status(job_ids)
 
-      responses = Sidekiq.redis do |redis|
+      responses.select(&:present?).count
+    end
+
+    # Returns the number of jobs that have completed.
+    #
+    # job_ids - The Sidekiq job IDs to check.
+    def self.num_completed(job_ids)
+      job_ids.size - self.num_running(job_ids)
+    end
+
+    # Returns the job status for each of the given job IDs.
+    #
+    # job_ids - The Sidekiq job IDs to check.
+    #
+    # Returns an array of true or false indicating job completion.
+    def self.job_status(job_ids)
+      keys = job_ids.map { |jid| key_for(jid) }
+
+      Sidekiq.redis do |redis|
         redis.pipelined do
           keys.each { |key| redis.exists(key) }
         end
       end
-
-      responses.all? { |value| !value }
     end
 
     def self.key_for(jid)
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index 22c39436cb2465dc290a0b85d6717f31b92eea26..cb7957e2af961eeceb14df20f5708539d3c8ec70 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -4,7 +4,7 @@ module Gitlab
     module Finders
       class RepoTemplateFinder < BaseTemplateFinder
         # Raised when file is not found
-        class FileNotFoundError < StandardError; end
+        FileNotFoundError = Class.new(StandardError)
 
         def initialize(project, base_dir, extension, categories = {})
           @categories     = categories
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 9d2ecee97569583d91eaa9c5659c789b45e5e097..fd040148a1e0c40da561e6a731b3d9aaa8200135 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -28,7 +28,7 @@ module Gitlab
         end
 
         def dropdown_names(context)
-          categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+          categories = context == 'autodeploy' ? ['Auto deploy'] : %w(General Pages)
           super().slice(*categories)
         end
       end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
index ce14cc887d0f4edfd674c9df14b2feeb2274d1e4..8947ecfb92ee39dcc51bb5be21a9c7db20dff3d3 100644
--- a/lib/gitlab/update_path_error.rb
+++ b/lib/gitlab/update_path_error.rb
@@ -1,3 +1,3 @@
 module Gitlab
-  class UpdatePathError < StandardError; end
+  UpdatePathError = Class.new(StandardError)
 end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index 4cc34e344606be2385d5a4cad9476329c3b433c7..961df0468a435264e41d6eff8a8cb656611854ca 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -46,7 +46,7 @@ module Gitlab
       git_tags = fetch_git_tags
       git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ }
       git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) }
-      "v#{git_versions.sort.last.to_s}"
+      "v#{git_versions.sort.last}"
     end
 
     def fetch_git_tags
@@ -59,10 +59,10 @@ module Gitlab
         "Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
         "Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
         "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
-        "Install gems" => %W(bundle),
-        "Migrate DB" => %W(bundle exec rake db:migrate),
-        "Recompile assets" => %W(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
-        "Clear cache" => %W(bundle exec rake cache:clear)
+        "Install gems" => %w(bundle),
+        "Migrate DB" => %w(bundle exec rake db:migrate),
+        "Recompile assets" => %w(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
+        "Clear cache" => %w(bundle exec rake cache:clear)
       }
     end
 
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e14a5666960c34f85602c7118cc17250e0d8fa4
--- /dev/null
+++ b/lib/gitlab/url_blocker.rb
@@ -0,0 +1,59 @@
+require 'resolv'
+
+module Gitlab
+  class UrlBlocker
+    class << self
+      # Used to specify what hosts and port numbers should be prohibited for project
+      # imports.
+      VALID_PORTS = [22, 80, 443].freeze
+
+      def blocked_url?(url)
+        return false if url.nil?
+
+        blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+        blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+        begin
+          uri = Addressable::URI.parse(url)
+          # Allow imports from the GitLab instance itself but only from the configured ports
+          return false if internal?(uri)
+
+          return true if blocked_port?(uri.port)
+
+          server_ips = Resolv.getaddresses(uri.hostname)
+          return true if (blocked_ips & server_ips).any?
+        rescue Addressable::URI::InvalidURIError
+          return true
+        end
+
+        false
+      end
+
+      private
+
+      def blocked_port?(port)
+        return false if port.blank?
+
+        port < 1024 && !VALID_PORTS.include?(port)
+      end
+
+      def internal?(uri)
+        internal_web?(uri) || internal_shell?(uri)
+      end
+
+      def internal_web?(uri)
+        uri.hostname == config.gitlab.host &&
+          (uri.port.blank? || uri.port == config.gitlab.port)
+      end
+
+      def internal_shell?(uri)
+        uri.hostname == config.gitlab_shell.ssh_host &&
+          (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
+      end
+
+      def config
+        Gitlab.config
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 19dad699edf4c50de81690add1b0e64dd790e6b7..9ce13feb79a73f7aab32219de5fa60ebb8445d3e 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,7 @@
 module Gitlab
   class UrlSanitizer
     def self.sanitize(content)
-      regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
+      regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
 
       content.gsub(regexp) { |url| new(url).masked_url }
     rescue Addressable::URI::InvalidURIError
@@ -9,6 +9,8 @@ module Gitlab
     end
 
     def self.valid?(url)
+      return false unless url
+
       Addressable::URI.parse(url.strip)
 
       true
@@ -16,6 +18,12 @@ module Gitlab
       false
     end
 
+    def self.http_credentials_for_user(user)
+      return {} unless user.respond_to?(:username)
+
+      { user: user.username }
+    end
+
     def initialize(url, credentials: nil)
       @url = Addressable::URI.parse(url.strip)
       @credentials = credentials
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 6ce9b22929435512b83b58d317e0a890ac5fed43..f260c0c535fcb797d78482cd89767d4827c6fd84 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,7 +8,7 @@ module Gitlab
     end
 
     def can_do_action?(action)
-      return false if no_user_or_blocked?
+      return false unless can_access_git?
 
       @permission_cache ||= {}
       @permission_cache[action] ||= user.can?(action, project)
@@ -19,7 +19,7 @@ module Gitlab
     end
 
     def allowed?
-      return false if no_user_or_blocked?
+      return false unless can_access_git?
 
       if user.requires_ldap_check? && user.try_obtain_ldap_lease
         return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -29,7 +29,7 @@ module Gitlab
     end
 
     def can_push_to_branch?(ref)
-      return false if no_user_or_blocked?
+      return false unless can_access_git?
 
       if project.protected_branch?(ref)
         return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
@@ -44,7 +44,7 @@ module Gitlab
     end
 
     def can_merge_to_branch?(ref)
-      return false if no_user_or_blocked?
+      return false unless can_access_git?
 
       if project.protected_branch?(ref)
         access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -55,15 +55,15 @@ module Gitlab
     end
 
     def can_read_project?
-      return false if no_user_or_blocked?
+      return false unless can_access_git?
 
       user.can?(:read_project, project)
     end
 
     private
 
-    def no_user_or_blocked?
-      user.nil? || user.blocked?
+    def can_access_git?
+      user && user.can?(:access_git)
     end
   end
 end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index a4e966e401663f7b9da18b3ffe5d1cb7ad95c49c..8f1d1fdc02eddd9e52d3e83a260aa0f8077b0142 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -33,8 +33,10 @@ module Gitlab
     PUBLIC   = 20 unless const_defined?(:PUBLIC)
 
     class << self
-      def values
-        options.values
+      delegate :values, to: :options
+
+      def string_values
+        string_options.keys
       end
 
       def options
@@ -45,6 +47,14 @@ module Gitlab
         }
       end
 
+      def string_options
+        {
+          'private'  => PRIVATE,
+          'internal' => INTERNAL,
+          'public'   => PUBLIC
+        }
+      end
+
       def highest_allowed_level
         restricted_levels = current_application_settings.restricted_visibility_levels
 
@@ -84,18 +94,39 @@ module Gitlab
 
         level_name
       end
+
+      def level_value(level)
+        return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+        string_options[level] || PRIVATE
+      end
+
+      def string_level(level)
+        string_options.key(level)
+      end
     end
 
     def private?
-      visibility_level_field == PRIVATE
+      visibility_level_value == PRIVATE
     end
 
     def internal?
-      visibility_level_field == INTERNAL
+      visibility_level_value == INTERNAL
     end
 
     def public?
-      visibility_level_field == PUBLIC
+      visibility_level_value == PUBLIC
+    end
+
+    def visibility_level_value
+      self[visibility_level_field]
+    end
+
+    def visibility
+      Gitlab::VisibilityLevel.string_level(visibility_level_value)
+    end
+
+    def visibility=(level)
+      self[visibility_level_field] = Gitlab::VisibilityLevel.level_value(level)
     end
   end
 end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c8872df8a935e54b424c1fcb0a0eb283a45ef4d7..eae1a0abf06baced27e1e6945179ca5b5b1d2f6f 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -4,10 +4,11 @@ require 'securerandom'
 
 module Gitlab
   class Workhorse
-    SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
-    VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
-    INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
-    INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+    SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'.freeze
+    VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
+    INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
+    INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
+    NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
 
     # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
     # bytes https://tools.ietf.org/html/rfc4868#section-2.6
@@ -154,6 +155,18 @@ module Gitlab
         Rails.root.join('.gitlab_workhorse_secret')
       end
 
+      def set_key_and_notify(key, value, expire: nil, overwrite: true)
+        Gitlab::Redis.with do |redis|
+          result = redis.set(key, value, ex: expire, nx: !overwrite)
+          if result
+            redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}")
+            value
+          else
+            redis.get(key)
+          end
+        end
+      end
+
       protected
 
       def encode(hash)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index e55c0d6ac49c6f7f837282e79f83682df9d93dd6..3d60618006c0b0948cafa28d9ae4bcaf487a116f 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -1,5 +1,5 @@
 module Mattermost
-  class ClientError < Mattermost::Error; end
+  ClientError = Class.new(Mattermost::Error)
 
   class Client
     attr_reader :user
@@ -26,7 +26,7 @@ module Mattermost
 
     def session_get(path, options = {})
       with_session do |session|
-        get(session, path, options)  
+        get(session, path, options)
       end
     end
 
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
index 014df175be09d87b0a3fd218363beaf6ca41cd5c..dee6deb7974861661d9f30838a7a7fdc3d23d42d 100644
--- a/lib/mattermost/error.rb
+++ b/lib/mattermost/error.rb
@@ -1,3 +1,3 @@
 module Mattermost
-  class Error < StandardError; end
+  Error = Class.new(StandardError)
 end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 377cb7b10211f4b8101d923d6fb99c0f0500cb32..688a79c04412c97bea41e5cbe7b83c8bf04a0a26 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -5,7 +5,7 @@ module Mattermost
     end
   end
 
-  class ConnectionError < Mattermost::Error; end
+  ConnectionError = Class.new(Mattermost::Error)
 
   # This class' prime objective is to obtain a session token on a Mattermost
   # instance with SSO configured where this GitLab instance is the provider.
@@ -153,7 +153,7 @@ module Mattermost
       yield
     rescue HTTParty::Error => e
       raise Mattermost::ConnectionError.new(e.message)
-    rescue Errno::ECONNREFUSED
+    rescue Errno::ECONNREFUSED => e
       raise Mattermost::ConnectionError.new(e.message)
     end
   end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 09dfd082b3a5e4fb211219a13a7445de1105310f..2cdbbdece1640d6659bf754dcbca5736410e8ffd 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,18 @@
 module Mattermost
   class Team < Client
+    # Returns **all** teams for an admin
     def all
-      session_get('/api/v3/teams/all')
+      session_get('/api/v3/teams/all').values
+    end
+
+    # Creates a team on the linked Mattermost instance, the team admin will be the
+    # `current_user` passed to the Mattermost::Client instance
+    def create(name:, display_name:, type:)
+      session_post('/api/v3/teams/create', body: {
+        name: name,
+        display_name: display_name,
+        type: type
+      }.to_json)
     end
   end
 end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
similarity index 100%
rename from lib/omniauth/strategies/bitbucket.rb
rename to lib/omni_auth/strategies/bitbucket.rb
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 4edfd0150740b2d28483f9767d1014d339ae841d..be0d97370d0a4514b853b7f7f730b124d042d15a 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -5,10 +5,10 @@ module Rouge
 
       # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
       #
-      # [+linenostart+]     The line number for the first line (default: 1).
-      def initialize(linenostart: 1)
-        @linenostart = linenostart
-        @line_number = linenostart
+      # [+tag+]     The tag (language) of the lexer used to generate the formatted tokens
+      def initialize(tag: nil)
+        @line_number = 1
+        @tag = tag
       end
 
       def stream(tokens, &b)
@@ -17,7 +17,7 @@ module Rouge
           yield "\n" unless is_first
           is_first = false
 
-          yield %(<span id="LC#{@line_number}" class="line">)
+          yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
           line.each { |token, value| yield span(token, value.chomp) }
           yield %(</span>)
 
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index e5797d8fe3cc9cca4d6ed545177bd1f5de74e27d..f6642527639c01e61e872c2f90fd4afb5785d283 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -56,14 +56,14 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
 # The value of -listen-http must be set to `gitlab.yml > pages > external_http`
 # as well. For example:
 #
-#   -listen-http 1.1.1.1:80
+#   -listen-http 1.1.1.1:80 -listen-http [2001::1]:80
 #
 # To enable HTTPS support for custom domains add the `-listen-https`,
 # `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
 # The value of -listen-https must be set to `gitlab.yml > pages > external_https`
 # as well. For example:
 #
-#   -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+#   -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
 #
 # The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
 # Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31f1e04b2b4be1a5c4a907b901b375a..f25e66d54c89a888fed6d9bef87246ec255f7498 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
 
   ## See app/controllers/application_controller.rb for headers set
 
+  ## Real IP Module Config
+  ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+  real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+  real_ip_recursive off;    ## If you enable 'on'
+  ## If you have a trusted IP address, uncomment it and set it
+  # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
   ## Individual nginx logs for this GitLab vhost
   access_log  /var/log/nginx/gitlab_access.log;
   error_log   /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5661394058db939834dc44df3ede3035fb27dac6..2b40da18babcbd3858f81895cc29e8888cdda970 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -82,6 +82,16 @@ server {
   ##
   # ssl_dhparam /etc/ssl/certs/dhparam.pem;
 
+  ## [Optional] Enable HTTP Strict Transport Security
+  # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+
+  ## Real IP Module Config
+  ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+  real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+  real_ip_recursive off;    ## If you enable 'on'
+  ## If you have a trusted IP address, uncomment it and set it
+  # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
   ## Individual nginx logs for this GitLab vhost
   access_log  /var/log/nginx/gitlab_access.log;
   error_log   /var/log/nginx/gitlab_error.log;
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index d5a402907d817ff0a71489280234a69e2d6d16df..2301ec9b2288c16b0a32701e40c679a8f107dfcd 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
 task :brakeman do
   # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
   # requests are welcome!
-  if system(*%W(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
+  if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
     puts 'Security check succeed'
   else
     puts 'Security check failed'
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 78ae187817aa5c80b842dfdcc21f4314c084c182..d55923673b1e1eca5d51c7201bd29bc546f8b60f 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,7 +1,7 @@
 namespace :cache do
   namespace :clear do
     REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
-    REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+    REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
 
     desc "GitLab | Clear redis cache"
     task redis: :environment do
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 5e94fba97bf1e8273e463575262d1844e4b5e73d..e65609d7001492b50cd3c3a31d4f72d83c41a441 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -2,7 +2,7 @@ task dev: ["dev:setup"]
 
 namespace :dev do
   desc "GitLab | Setup developer environment (db, fixtures)"
-  task :setup => :environment do
+  task setup: :environment do
     ENV['force'] = 'yes'
     Rake::Task["gitlab:setup"].invoke
     Rake::Task["gitlab:shell:setup"].invoke
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
index afe5d42910c281140719c5f9809f003a8f5c56a6..557f4fef10b3831399e21be18d2a6de8069f0073 100644
--- a/lib/tasks/downtime_check.rake
+++ b/lib/tasks/downtime_check.rake
@@ -1,10 +1,10 @@
 desc 'Checks if migrations in a branch require downtime'
 task downtime_check: :environment do
-  if defined?(Gitlab::License)
-    repo = 'gitlab-ee'
-  else
-    repo = 'gitlab-ce'
-  end
+  repo = if defined?(Gitlab::License)
+           'gitlab-ee'
+         else
+           'gitlab-ce'
+         end
 
   `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
 
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index e9587595fef45e9f4840cdc251381557af2801d6..7ad2b2e4d39afe266625648d7d27666dc14b40d0 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
 desc 'Code duplication analyze via flay'
 task :flay do
-  output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+  output = `bundle exec flay --mass 35 app/ lib/gitlab/`
 
   if output.include? "Similar code found"
     puts output
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 993112aee3be341125b045c64e91d7747f1ff8d4..5293f5af12dd2ee1138982fb60c875405d242c97 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,33 +1,36 @@
 namespace :gemojione do
   desc 'Generates Emoji SHA256 digests'
-  task digests: :environment do
+  task digests: ['yarn:check', 'environment'] do
     require 'digest/sha2'
     require 'json'
 
-    dir = Gemojione.images_path
-    digests = []
-    aliases = Hash.new { |hash, key| hash[key] = [] }
-    aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
-
-    JSON.parse(File.read(aliases_path)).each do |alias_name, real_name|
-      aliases[real_name] << alias_name
-    end
-
-    Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
-      fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
-      digest = Digest::SHA256.file(fpath).hexdigest
+    # We don't have `node_modules` available in built versions of GitLab
+    FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis'))
 
-      digests << { name: name, unicode: emoji_hash['unicode'], digest: digest }
+    dir = Gemojione.images_path
+    resultant_emoji_map = {}
+
+    Gitlab::Emoji.emojis.each do |name, emoji_hash|
+      # Ignore aliases
+      unless Gitlab::Emoji.emojis_aliases.key?(name)
+        fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+        hash_digest = Digest::SHA256.file(fpath).hexdigest
+
+        entry = {
+          category: emoji_hash['category'],
+          moji: emoji_hash['moji'],
+          unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
+          digest: hash_digest,
+        }
 
-      aliases[name].each do |alias_name|
-        digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
+        resultant_emoji_map[name] = entry
       end
     end
 
     out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
 
     File.open(out, 'w') do |handle|
-      handle.write(JSON.pretty_generate(digests))
+      handle.write(JSON.pretty_generate(resultant_emoji_map))
     end
   end
 
@@ -55,21 +58,40 @@ namespace :gemojione do
     SPRITESHEET_WIDTH = 860
     SPRITESHEET_HEIGHT = 840
 
+    # Setup a map to rename image files
+    emoji_unicode_string_to_name_map = {}
+    Gitlab::Emoji.emojis.each do |name, emoji_hash|
+      # Ignore aliases
+      unless Gitlab::Emoji.emojis_aliases.key?(name)
+        emoji_unicode_string_to_name_map[emoji_hash['unicode']] = name
+      end
+    end
+
+    # Copy the Gemojione assets to the temporary folder for renaming
+    emoji_dir = "app/assets/images/emoji"
+    FileUtils.rm_rf(emoji_dir)
+    FileUtils.mkdir_p(emoji_dir, mode: 0700)
+    FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
+    Dir[File.join(emoji_dir, "**/*.png")].each do |png|
+      image_path = png
+      rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+    end
+
     Dir.mktmpdir do |tmpdir|
-      # Copy the Gemojione assets to the temporary folder for resizing
-      FileUtils.cp_r(Gemojione.images_path, tmpdir)
+      FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
 
       Dir.chdir(tmpdir) do
         Dir["**/*.png"].each do |png|
-          resize!(File.join(tmpdir, png), SIZE)
+          tmp_image_path = File.join(tmpdir, png)
+          resize!(tmp_image_path, SIZE)
         end
       end
 
-      style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
+      style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
 
       # Combine the resized assets into a packed sprite and re-generate the SCSS
       SpriteFactory.cssurl = "image-url('$IMAGE')"
-      SpriteFactory.run!(File.join(tmpdir, 'png'), {
+      SpriteFactory.run!(tmpdir, {
         output_style: style_path,
         output_image: "app/assets/images/emoji.png",
         selector:     '.emoji-',
@@ -83,7 +105,7 @@ namespace :gemojione do
       # let's simplify it
       system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
       system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
-      system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
+      system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
 
       # Append a generic rule that applies to all Emojis
       File.open(style_path, 'a') do |f|
@@ -92,6 +114,8 @@ namespace :gemojione do
         .emoji-icon {
           background-image: image-url('emoji.png');
           background-repeat: no-repeat;
+          color: transparent;
+          text-indent: -99em;
           height: #{SIZE}px;
           width: #{SIZE}px;
 
@@ -112,16 +136,17 @@ namespace :gemojione do
     # Now do it again but for Retina
     Dir.mktmpdir do |tmpdir|
       # Copy the Gemojione assets to the temporary folder for resizing
-      FileUtils.cp_r(Gemojione.images_path, tmpdir)
+      FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
 
       Dir.chdir(tmpdir) do
         Dir["**/*.png"].each do |png|
-          resize!(File.join(tmpdir, png), RETINA)
+          tmp_image_path = File.join(tmpdir, png)
+          resize!(tmp_image_path, RETINA)
         end
       end
 
       # Combine the resized assets into a packed sprite and re-generate the SCSS
-      SpriteFactory.run!(File.join(tmpdir), {
+      SpriteFactory.run!(tmpdir, {
         output_image: "app/assets/images/emoji@2x.png",
         style:        false,
         nocomments:   true,
@@ -155,4 +180,20 @@ namespace :gemojione do
     image.write(image_path) { self.quality = 100 }
     image.destroy!
   end
+
+  EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
+  def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+    # Rename file from unicode to emoji name
+    matches = EMOJI_IMAGE_PATH_RE.match(image_path)
+    preceding_path = matches[1]
+    unicode_string = matches[2]
+    name = emoji_unicode_string_to_name_map[unicode_string]
+    if name
+      new_png_path = File.join(preceding_path, "#{name}.png")
+      FileUtils.mv(image_path, new_png_path)
+      new_png_path
+    else
+      puts "Warning: emoji_unicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
+    end
+  end
 end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 3eb5fc07b3c20a562e774b3dd1ad53017f0da6a2..098f9851b451a7672af517fa50a97cfe0038bd5c 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -20,7 +20,7 @@ namespace :gitlab do
     desc 'GitLab | Assets | Fix all absolute url references in CSS'
     task :fix_urls do
       css_files = Dir['public/assets/*.css']
-      css_files.each do | file |
+      css_files.each do |file|
         # replace url(/assets/*) with url(./*)
         puts "Fixing #{file}"
         system "sed", "-i", "-e", 's/url(\([\"\']\?\)\/assets\//url(\1.\//g', file
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 6102517e730428d7426b21b4cff5553f80745a63..a6f8c4ced5d9cda532172f3c1e8787ce8ba9b402 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -6,8 +6,6 @@ namespace :gitlab do
                  gitlab:ldap:check
                  gitlab:app:check}
 
-
-
   namespace :app do
     desc "GitLab | Check the configuration of the GitLab Rails app"
     task check: :environment  do
@@ -34,7 +32,6 @@ namespace :gitlab do
       finished_checking "GitLab"
     end
 
-
     # Checks
     ########################
 
@@ -194,7 +191,7 @@ namespace :gitlab do
     def check_migrations_are_up
       print "All migrations up? ... "
 
-      migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
+      migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
 
       unless migration_status =~ /down\s+\d{14}/
         puts "yes".color(:green)
@@ -279,7 +276,7 @@ namespace :gitlab do
       upload_path_tmp = File.join(upload_path, 'tmp')
 
       if File.stat(upload_path).mode == 040700
-        unless Dir.exists?(upload_path_tmp)
+        unless Dir.exist?(upload_path_tmp)
           puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
           return
         end
@@ -316,7 +313,7 @@ namespace :gitlab do
       min_redis_version = "2.8.0"
       print "Redis version >= #{min_redis_version}? ... "
 
-      redis_version = run_command(%W(redis-cli --version))
+      redis_version = run_command(%w(redis-cli --version))
       redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
       if redis_version &&
           (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -351,14 +348,14 @@ namespace :gitlab do
       finished_checking "GitLab Shell"
     end
 
-
     # Checks
     ########################
 
     def check_repo_base_exists
       puts "Repo base directory exists?"
 
-      Gitlab.config.repositories.storages.each do |name, repo_base_path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        repo_base_path = repository_storage['path']
         print "#{name}... "
 
         if File.exist?(repo_base_path)
@@ -382,12 +379,13 @@ namespace :gitlab do
     def check_repo_base_is_not_symlink
       puts "Repo storage directories are symlinks?"
 
-      Gitlab.config.repositories.storages.each do |name, repo_base_path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        repo_base_path = repository_storage['path']
         print "#{name}... "
 
         unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
-          return
+          break
         end
 
         unless File.symlink?(repo_base_path)
@@ -405,12 +403,13 @@ namespace :gitlab do
     def check_repo_base_permissions
       puts "Repo paths access is drwxrws---?"
 
-      Gitlab.config.repositories.storages.each do |name, repo_base_path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        repo_base_path = repository_storage['path']
         print "#{name}... "
 
         unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
-          return
+          break
         end
 
         if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
@@ -435,12 +434,13 @@ namespace :gitlab do
       gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
       puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
 
-      Gitlab.config.repositories.storages.each do |name, repo_base_path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        repo_base_path = repository_storage['path']
         print "#{name}... "
 
         unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
-          return
+          break
         end
 
         uid = uid_for(gitlab_shell_ssh_user)
@@ -493,7 +493,6 @@ namespace :gitlab do
           )
           fix_and_rerun
         end
-
       end
     end
 
@@ -565,8 +564,6 @@ namespace :gitlab do
     end
   end
 
-
-
   namespace :sidekiq do
     desc "GitLab | Check the configuration of Sidekiq"
     task check: :environment  do
@@ -579,7 +576,6 @@ namespace :gitlab do
       finished_checking "Sidekiq"
     end
 
-
     # Checks
     ########################
 
@@ -621,12 +617,11 @@ namespace :gitlab do
     end
 
     def sidekiq_process_count
-      ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+      ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
       ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
     end
   end
 
-
   namespace :incoming_email do
     desc "GitLab | Check the configuration of Reply by email"
     task check: :environment  do
@@ -649,7 +644,6 @@ namespace :gitlab do
       finished_checking "Reply by email"
     end
 
-
     # Checks
     ########################
 
@@ -757,7 +751,7 @@ namespace :gitlab do
     end
 
     def mail_room_running?
-      ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+      ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
       ps_ux.include?("mail_room")
     end
   end
@@ -805,13 +799,13 @@ namespace :gitlab do
     def check_ldap_auth(adapter)
       auth = adapter.config.has_auth?
 
-      if auth && adapter.ldap.bind
-        message = 'Success'.color(:green)
-      elsif auth
-        message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
-      else
-        message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
-      end
+      message = if auth && adapter.ldap.bind
+                  'Success'.color(:green)
+                elsif auth
+                  'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+                else
+                  'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+                end
 
       puts "LDAP authentication... #{message}"
     end
@@ -820,8 +814,8 @@ namespace :gitlab do
   namespace :repo do
     desc "GitLab | Check the integrity of the repositories managed by GitLab"
     task check: :environment do
-      Gitlab.config.repositories.storages.each do |name, path|
-        namespace_dirs = Dir.glob(File.join(path, '*'))
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*'))
 
         namespace_dirs.each do |namespace_dir|
           repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
@@ -838,11 +832,11 @@ namespace :gitlab do
       user = User.find_by(username: username)
       if user
         repo_dirs = user.authorized_projects.map do |p|
-                      File.join(
-                        p.repository_storage_path,
-                        "#{p.path_with_namespace}.git"
-                      )
-                    end
+          File.join(
+            p.repository_storage_path,
+            "#{p.path_with_namespace}.git"
+          )
+        end
 
         repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
       else
@@ -855,7 +849,7 @@ namespace :gitlab do
   ##########################
 
   def fix_and_rerun
-    puts "  Please #{"fix the error above"} and rerun the checks.".color(:red)
+    puts "  Please fix the error above and rerun the checks.".color(:red)
   end
 
   def for_more_information(*sources)
@@ -917,7 +911,7 @@ namespace :gitlab do
 
   def check_ruby_version
     required_version = Gitlab::VersionInfo.new(2, 1, 0)
-    current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
+    current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
 
     print "Ruby version >= #{required_version} ? ... "
 
@@ -988,13 +982,13 @@ namespace :gitlab do
   end
 
   def check_config_lock(repo_dir)
-    config_exists = File.exist?(File.join(repo_dir,'config.lock'))
+    config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
     config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
     puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
   end
 
   def check_ref_locks(repo_dir)
-    lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
+    lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
     if lock_files.present?
       puts "Ref lock files exist:".color(:red)
       lock_files.each do |lock_file|
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 967f630ef203379e282525c287268f21f8bd5704..f76bef5f4bfc85b5c45301c23f3a2c495eb3e4f7 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -6,7 +6,8 @@ namespace :gitlab do
       remove_flag = ENV['REMOVE']
 
       namespaces = Namespace.pluck(:path)
-      Gitlab.config.repositories.storages.each do |name, git_base_path|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        git_base_path = repository_storage['path']
         all_dirs = Dir.glob(git_base_path + '/*')
 
         puts git_base_path.color(:yellow)
@@ -25,7 +26,6 @@ namespace :gitlab do
         end
 
         all_dirs.each do |dir_path|
-
           if remove_flag
             if FileUtils.rm_rf dir_path
               puts "Removed...#{dir_path}".color(:red)
@@ -48,16 +48,17 @@ namespace :gitlab do
       warn_user_is_not_gitlab
 
       move_suffix = "+orphaned+#{Time.now.to_i}"
-      Gitlab.config.repositories.storages.each do |name, repo_root|
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        repo_root = repository_storage['path']
         # Look for global repos (legacy, depth 1) and normal repos (depth 2)
         IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
           find.each_line do |path|
             path.chomp!
-            repo_with_namespace = path.
-              sub(repo_root, '').
-              sub(%r{^/*}, '').
-              chomp('.git').
-              chomp('.wiki')
+            repo_with_namespace = path
+              .sub(repo_root, '')
+              .sub(%r{^/*}, '')
+              .chomp('.git')
+              .chomp('.wiki')
             next if Project.find_by_full_path(repo_with_namespace)
             new_path = path + move_suffix
             puts path.inspect + ' -> ' + new_path.inspect
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 7c96bc864ce2363159e3abb25078da59c2daf00f..5476438b8fa3b5345c392363eba1092a45c8a385 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
     end
 
     desc 'Drop all tables'
-    task :drop_tables => :environment do
+    task drop_tables: :environment do
       connection = ActiveRecord::Base.connection
 
       # If MySQL, turn off foreign key checks
@@ -62,9 +62,9 @@ namespace :gitlab do
 
       ref = Shellwords.escape(args[:ref])
 
-      migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines.
-        map { |file| Rails.root.join(file.strip).to_s }.
-        select { |file| File.file?(file) }
+      migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines
+        .map { |file| Rails.root.join(file.strip).to_s }
+        .select { |file| File.file?(file) }
 
       Gitlab::DowntimeCheck.new.check_and_print(migrations)
     end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7db0779def81540d14432fcb76b3c0ea7ada0aeb..7ccda04a35f9f45898e6fdc518d78b76ba628b0b 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,7 @@ namespace :gitlab do
     task :ee_compat_check, [:branch] => :environment do |_, args|
       opts =
         if ENV['CI']
-          { branch: ENV['CI_BUILD_REF_NAME'] }
+          { branch: ENV['CI_COMMIT_REF_NAME'] }
         else
           unless args[:branch]
             puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index a67c1fe1f279ffb3c519c89c5693e057f02c365d..cf82134d97ec2fa403f04fb8819e5b732d05c73d 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -1,6 +1,5 @@
 namespace :gitlab do
   namespace :git do
-
     desc "GitLab | Git | Repack"
     task repack: :environment do
       failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
@@ -50,6 +49,5 @@ namespace :gitlab do
       puts "The following repositories reported errors:".color(:red)
       failures.each { |f| puts "- #{f}" }
     end
-
   end
 end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index b4015f5238e548f7d0ed128402ce52b0ad3cb8ff..48bd9139ce87f60248f2843ae4e0ab719af94edf 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -11,7 +11,8 @@ namespace :gitlab do
     #
     desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
     task repos: :environment do
-      Gitlab.config.repositories.storages.each do |name, git_base_path|
+      Gitlab.config.repositories.storages.each_value do |repository_storage|
+        git_base_path = repository_storage['path']
         repos_to_import = Dir.glob(git_base_path + '/**/*.git')
 
         repos_to_import.each do |repo_path|
@@ -46,7 +47,7 @@ namespace :gitlab do
               group = Namespace.find_by(path: group_name)
               # create group namespace
               unless group
-                group = Group.new(:name => group_name)
+                group = Group.new(name: group_name)
                 group.path = group_name
                 group.owner = user
                 if group.save
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index c2c6031db670d269f9726ef1f1ca693d84a3b986..dd1825c8a9e843b937bec6119b45df808253e204 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
 
     desc "GitLab | Display exported DB structure"
     task data: :environment do
-      puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+      puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
     end
   end
 end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index f7c831892ee28e89eb51ffcc9c37450c5559640e..a2a2db487b71da2bb62073b497fcbf0414ecc743 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -2,24 +2,25 @@ namespace :gitlab do
   namespace :env do
     desc "GitLab | Show information about GitLab and its environment"
     task info: :environment  do
-
       # check if there is an RVM environment
-      rvm_version = run_and_match(%W(rvm --version), /[\d\.]+/).try(:to_s)
+      rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
       # check Ruby version
-      ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
+      ruby_version = run_and_match(%w(ruby --version), /[\d\.p]+/).try(:to_s)
       # check Gem version
-      gem_version = run_command(%W(gem --version))
+      gem_version = run_command(%w(gem --version))
       # check Bundler version
-      bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
+      bunder_version = run_and_match(%w(bundle --version), /[\d\.]+/).try(:to_s)
       # check Rake version
-      rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+      rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s)
       # check redis version
-      redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+      redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+      # check Git version
+      git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a
 
       puts ""
       puts "System information".color(:yellow)
       puts "System:\t\t#{os_name || "unknown".color(:red)}"
-      puts "Current User:\t#{run_command(%W(whoami))}"
+      puts "Current User:\t#{run_command(%w(whoami))}"
       puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
       puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
       puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
@@ -27,9 +28,9 @@ namespace :gitlab do
       puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
       puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
       puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
+      puts "Git Version:\t#{git_version[1] || "unknown".color(:red)}"
       puts "Sidekiq Version:#{Sidekiq::VERSION}"
 
-
       # check database adapter
       database_adapter = ActiveRecord::Base.connection.adapter_name.downcase
 
@@ -54,8 +55,6 @@ namespace :gitlab do
       puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
       puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
 
-
-
       # check Gitolite version
       gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION"
       if File.readable?(gitlab_shell_version_file)
@@ -66,12 +65,11 @@ namespace :gitlab do
       puts "GitLab Shell".color(:yellow)
       puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
       puts "Repository storage paths:"
-      Gitlab.config.repositories.storages.each do |name, path|
-        puts "- #{name}: \t#{path}"
+      Gitlab.config.repositories.storages.each do |name, repository_storage|
+        puts "- #{name}: \t#{repository_storage['path']}"
       end
       puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
       puts "Git:\t\t#{Gitlab.config.git.bin_path}"
-
     end
   end
 end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 5a09cd7ce41decaaac0e1945afdf0c81acc69f15..dd2fda54e622922c61d96b0a05b8d9e3954278fa 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -20,10 +20,10 @@ namespace :gitlab do
         config = {
           user: Gitlab.config.gitlab.user,
           gitlab_url: gitlab_url,
-          http_settings: {self_signed_cert: false}.stringify_keys,
+          http_settings: { self_signed_cert: false }.stringify_keys,
           auth_file: File.join(user_home, ".ssh", "authorized_keys"),
           redis: {
-            bin: %x{which redis-cli}.chomp,
+            bin: `which redis-cli`.chomp,
             namespace: "resque:gitlab"
           }.stringify_keys,
           log_level: "INFO",
@@ -43,7 +43,7 @@ namespace :gitlab do
         File.open("config.yml", "w+") {|f| f.puts config.to_yaml}
 
         # Launch installation process
-        system(*%W(bin/install) + repository_storage_paths_args)
+        system(*%w(bin/install) + repository_storage_paths_args)
       end
 
       # (Re)create hooks
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index f2e12d850454472a4be54e7e8d4cbcf53e1c2eaf..6cbc83b89732ef5e58146d4a804a2a853bfccef0 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -1,9 +1,9 @@
 namespace :gitlab do
   namespace :sidekiq do
-    QUEUE = 'queue:post_receive'
+    QUEUE = 'queue:post_receive'.freeze
 
     desc 'Drop all Sidekiq PostReceive jobs for a given project'
-    task :drop_post_receive , [:project] => :environment do |t, args|
+    task :drop_post_receive, [:project] => :environment do |t, args|
       unless args.project.present?
         abort "Please specify the project you want to drop PostReceive jobs for:\n  rake gitlab:sidekiq:drop_post_receive[group/project]"
       end
@@ -21,7 +21,7 @@ namespace :gitlab do
         # new jobs already. We will repopulate it with the old jobs, skipping the
         # ones we want to drop.
         dropped = 0
-        while (job = redis.lpop(temp_queue)) do
+        while (job = redis.lpop(temp_queue))
           if repo_path(job) == project_path
             dropped += 1
           else
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e128738b5f8cb2ca92a9a6f0ffcc8e4037bfe8fb..bb755ae689b98a76141f3db73b5eb4345ac00bc4 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -19,23 +19,20 @@ module Gitlab
     # It will primarily use lsb_relase to determine the OS.
     # It has fallbacks to Debian, SuSE, OS X and systems running systemd.
     def os_name
-      os_name = run_command(%W(lsb_release -irs))
-      os_name ||= if File.readable?('/etc/system-release')
-        File.read('/etc/system-release')
-      end
-      os_name ||= if File.readable?('/etc/debian_version')
-        debian_version = File.read('/etc/debian_version')
-        "Debian #{debian_version}"
-      end
-      os_name ||= if File.readable?('/etc/SuSE-release')
-        File.read('/etc/SuSE-release')
-      end
-      os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
-        "Mac OS X #{os_x_version}"
-      end
-      os_name ||= if File.readable?('/etc/os-release')
-        File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
-      end
+      os_name = run_command(%w(lsb_release -irs))
+      os_name ||=
+        if File.readable?('/etc/system-release')
+          File.read('/etc/system-release')
+        elsif File.readable?('/etc/debian_version')
+          "Debian #{File.read('/etc/debian_version')}"
+        elsif File.readable?('/etc/SuSE-release')
+          File.read('/etc/SuSE-release')
+        elsif os_x_version = run_command(%w(sw_vers -productVersion))
+          "Mac OS X #{os_x_version}"
+        elsif File.readable?('/etc/os-release')
+          File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
+        end
+
       os_name.try(:squish!)
     end
 
@@ -104,7 +101,7 @@ module Gitlab
     def warn_user_is_not_gitlab
       unless @warned_user_not_gitlab
         gitlab_user = Gitlab.config.gitlab.user
-        current_user = run_command(%W(whoami)).chomp
+        current_user = run_command(%w(whoami)).chomp
         unless current_user == gitlab_user
           puts " Warning ".color(:black).background(:yellow)
           puts "  You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
@@ -133,8 +130,8 @@ module Gitlab
     end
 
     def all_repos
-      Gitlab.config.repositories.storages.each do |name, path|
-        IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+      Gitlab.config.repositories.storages.each_value do |repository_storage|
+        IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
           find.each_line do |path|
             yield path.chomp
           end
@@ -143,7 +140,7 @@ module Gitlab
     end
 
     def repository_storage_paths_args
-      Gitlab.config.repositories.storages.values
+      Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
     end
 
     def user_home
@@ -171,14 +168,14 @@ module Gitlab
 
     def reset_to_tag(tag_wanted, target_dir)
       tag =
-      begin
-        # First try to checkout without fetching
-        # to avoid stalling tests if the Internet is down.
-        run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
-      rescue Gitlab::TaskFailedError
-        run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
-        run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
-      end
+        begin
+          # First try to checkout without fetching
+          # to avoid stalling tests if the Internet is down.
+          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
+        rescue Gitlab::TaskFailedError
+          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
+          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
+        end
 
       if tag
         run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 84810b489ceecd99171a37d97aeaabf30ecc1ae7..523b0fa055b7ea4085f894c75a8f3d3daf6684e1 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -2,15 +2,15 @@ namespace :gitlab do
   desc "GitLab | Run all tests"
   task :test do
     cmds = [
-      %W(rake brakeman),
-      %W(rake rubocop),
-      %W(rake spinach),
-      %W(rake spec),
-      %W(rake karma)
+      %w(rake brakeman),
+      %w(rake rubocop),
+      %w(rake spinach),
+      %w(rake spec),
+      %w(rake karma)
     ]
 
     cmds.each do |cmd|
-      system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+      system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
     end
   end
 end
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
index 84aa2e8507a16c67b7c3ff2437b99974d4849e36..6f101aea3037e126118494ab87be58976c5a45b8 100644
--- a/lib/tasks/gitlab/track_deployment.rake
+++ b/lib/tasks/gitlab/track_deployment.rake
@@ -1,8 +1,8 @@
 namespace :gitlab do
   desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
   task track_deployment: :environment do
-    metric = Gitlab::Metrics::Metric.
-      new('deployments', version: Gitlab::VERSION)
+    metric = Gitlab::Metrics::Metric
+      .new('deployments', version: Gitlab::VERSION)
 
     Gitlab::Metrics.submit_metrics([metric.to_hash])
   end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index b77a5bb62d1435619eb259472dc3f30d31b3c8d7..dbdfb335a5cf7493daacea166283f0adc6e6a347 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
       "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
       /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
     )
-  ]
+  ].freeze
 
   def vendor_directory
     Rails.root.join('vendor')
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 49530e7a37202fcc6144e0c3adbd5cfa94376c93..5a1c8006052ccd4b615e02c0bf2d367ee6b89cf8 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,7 +1,7 @@
 namespace :gitlab do
   namespace :web_hook do
     desc "GitLab | Adds a webhook to the projects"
-    task :add => :environment do
+    task add: :environment do
       web_hook_url = ENV['URL']
       namespace_path = ENV['NAMESPACE']
 
@@ -21,7 +21,7 @@ namespace :gitlab do
     end
 
     desc "GitLab | Remove a webhook from the projects"
-    task :rm => :environment do
+    task rm: :environment do
       web_hook_url = ENV['URL']
       namespace_path = ENV['NAMESPACE']
 
@@ -34,7 +34,7 @@ namespace :gitlab do
     end
 
     desc "GitLab | List webhooks"
-    task :list => :environment do
+    task list: :environment do
       namespace_path = ENV['NAMESPACE']
 
       projects = find_projects(namespace_path)
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 32b668df3bf1a85ea0a2ded4938805c394f89431..7b63e93db0e67a43941f6b41f94fab5ed991a47a 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -6,4 +6,3 @@ unless Rails.env.production?
     end
   end
 end
-
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index 4f2486157b74da0927c4eb2aa7c14de8fe6bfc45..fc2cea8c0167f42c1f2e3e064d691a93006b4a40 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -24,7 +24,7 @@ task migrate_iids: :environment do
       else
         print 'F'
       end
-    rescue => ex
+    rescue
       print 'F'
     end
   end
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 39541c0b9c69a978301177eecf6bea123a207775..56b81106c5ffddf6a9f2b7124439e1f5aa4c9cea 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -76,23 +76,23 @@ namespace :services do
         end
 
         param_hash
-      end.sort_by { |p| p[:required] ? 0 : 1 }
+      end
+      service_hash[:params].sort_by! { |p| p[:required] ? 0 : 1 }
 
-      puts "Collected data for: #{service.title}, #{Time.now-service_start}"
+      puts "Collected data for: #{service.title}, #{Time.now - service_start}"
       service_hash
     end
 
     doc_start = Time.now
     doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
 
-    result = ERB.new(services_template, 0 , '>')
+    result = ERB.new(services_template, 0, '>')
       .result(OpenStruct.new(services: services).instance_eval { binding })
 
     File.open(doc_path, 'w') do |f|
       f.write result
     end
 
-    puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}"
-
+    puts "write a new service.md to: #{doc_path}, #{Time.now - doc_start}"
   end
 end
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
index d1f6ed87704f465fa2e427ec49482068bff0fb56..dd9ce86f7cab00f3d696377b42ddb75d672ccdc6 100644
--- a/lib/tasks/sidekiq.rake
+++ b/lib/tasks/sidekiq.rake
@@ -1,21 +1,21 @@
 namespace :sidekiq do
   desc "GitLab | Stop sidekiq"
   task :stop do
-    system *%W(bin/background_jobs stop)
+    system(*%w(bin/background_jobs stop))
   end
 
   desc "GitLab | Start sidekiq"
   task :start do
-    system *%W(bin/background_jobs start)
+    system(*%w(bin/background_jobs start))
   end
 
   desc 'GitLab | Restart sidekiq'
   task :restart do
-    system *%W(bin/background_jobs restart)
+    system(*%w(bin/background_jobs restart))
   end
 
   desc "GitLab | Start sidekiq with launchd on Mac OS X"
   task :launchd do
-    system *%W(bin/background_jobs start_no_deamonize)
+    system(*%w(bin/background_jobs start_no_deamonize))
   end
 end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 2cf7a25a0fde15226c7e2ce3e0cb8e7a9ea23b5d..602c60be8281fee29348bd845596331038080b27 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -4,8 +4,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run request specs'
   task :api do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag @api)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag @api)
     ]
     run_commands(cmds)
   end
@@ -13,8 +13,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run feature specs'
   task :feature do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag @feature)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag @feature)
     ]
     run_commands(cmds)
   end
@@ -22,8 +22,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run model specs'
   task :models do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag @models)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag @models)
     ]
     run_commands(cmds)
   end
@@ -31,8 +31,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run service specs'
   task :services do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag @services)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag @services)
     ]
     run_commands(cmds)
   end
@@ -40,8 +40,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run lib specs'
   task :lib do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag @lib)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag @lib)
     ]
     run_commands(cmds)
   end
@@ -49,8 +49,8 @@ namespace :spec do
   desc 'GitLab | Rspec | Run other specs'
   task :other do
     cmds = [
-      %W(rake gitlab:setup),
-      %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
+      %w(rake gitlab:setup),
+      %w(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
     ]
     run_commands(cmds)
   end
@@ -59,14 +59,14 @@ end
 desc "GitLab | Run specs"
 task :spec do
   cmds = [
-    %W(rake gitlab:setup),
-    %W(rspec spec),
+    %w(rake gitlab:setup),
+    %w(rspec spec),
   ]
   run_commands(cmds)
 end
 
 def run_commands(cmds)
   cmds.each do |cmd|
-    system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+    system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
   end
 end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 8dbfa7751dcf252c4f7541091c5fffce6e38a470..19ff13f06c04ab896215f856b1c5f82be694734e 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -35,7 +35,7 @@ task :spinach do
 end
 
 def run_system_command(cmd)
-  system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
+  system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
 end
 
 def run_spinach_command(args)
diff --git a/package.json b/package.json
index ad0aaef1897115e86d4e1ed555e93dea6e804e47..b3d038bd3d1a1b203bba2d361c579b11305c2838 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,11 @@
   "private": true,
   "scripts": {
     "dev-server": "webpack-dev-server --config config/webpack.config.js",
-    "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
-    "eslint-fix": "eslint --max-warnings 0 --ext .js,.js.es6 --fix .",
-    "eslint-report": "eslint --max-warnings 0 --ext .js,.js.es6 --format html --output-file ./eslint-report.html .",
+    "eslint": "eslint --max-warnings 0 --ext .js .",
+    "eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
+    "eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
     "karma": "karma start config/karma.config.js --single-run",
+    "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
     "karma-start": "karma start config/karma.config.js",
     "webpack": "webpack --config config/webpack.config.js",
     "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
@@ -13,26 +14,31 @@
   "dependencies": {
     "babel-core": "^6.22.1",
     "babel-loader": "^6.2.10",
-    "babel-preset-es2015": "^6.22.0",
+    "babel-plugin-transform-define": "^1.2.0",
+    "babel-preset-latest": "^6.24.0",
     "babel-preset-stage-2": "^6.22.0",
     "bootstrap-sass": "^3.3.6",
     "compression-webpack-plugin": "^0.3.2",
+    "core-js": "^2.4.1",
     "d3": "^3.5.11",
+    "document-register-element": "^1.3.0",
     "dropzone": "^4.2.0",
-    "es6-promise": "^4.0.5",
+    "emoji-unicode-version": "^0.2.1",
     "jquery": "^2.2.1",
-    "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4",
     "jquery-ujs": "^1.2.1",
     "js-cookie": "^2.1.3",
     "mousetrap": "^1.4.6",
     "pikaday": "^1.5.1",
+    "raphael": "^2.2.7",
+    "raw-loader": "^0.5.1",
     "select2": "3.5.2-browserify",
     "stats-webpack-plugin": "^0.4.3",
     "timeago.js": "^2.0.5",
     "underscore": "^1.8.3",
-    "vue": "^2.0.3",
+    "vue": "^2.1.10",
     "vue-resource": "^0.9.3",
-    "webpack": "^2.2.1"
+    "webpack": "^2.2.1",
+    "webpack-bundle-analyzer": "^2.3.0"
   },
   "devDependencies": {
     "babel-plugin-istanbul": "^4.0.0",
@@ -53,13 +59,5 @@
     "karma-sourcemap-loader": "^0.3.7",
     "karma-webpack": "^2.0.2",
     "webpack-dev-server": "^2.3.0"
-  },
-  "nyc": {
-    "exclude": [
-      "spec/javascripts/test_bundle.js",
-      "spec/javascripts/**/*_spec.js",
-      "spec/javascripts/**/*_spec.js.es6",
-      "app/assets/javascripts/droplab/**/*"
-    ]
   }
 }
diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg
deleted file mode 100644
index 922e28bf696ddf6250182550e12d29ae9fc34591..0000000000000000000000000000000000000000
--- a/public/ci/build-canceled.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg
deleted file mode 100644
index 1aefd3f17610a6535d77ac44dbe28e1b794e8893..0000000000000000000000000000000000000000
--- a/public/ci/build-failed.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg
deleted file mode 100644
index 536931af84d88a39e03c6309468eb9d4933aa53d..0000000000000000000000000000000000000000
--- a/public/ci/build-pending.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg
deleted file mode 100644
index 0d71eef3c344f21cb6150bd9fa45547b377034d3..0000000000000000000000000000000000000000
--- a/public/ci/build-running.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg
deleted file mode 100644
index f15507188e07289adf01143851d83bfcd5ca63c6..0000000000000000000000000000000000000000
--- a/public/ci/build-skipped.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">skipped</text><text x="66" y="14">skipped</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg
deleted file mode 100644
index 43b67e45f426f58bbb72aa3a3acb468e6e82e41b..0000000000000000000000000000000000000000
--- a/public/ci/build-success.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>
\ No newline at end of file
diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg
deleted file mode 100644
index c72a2f5a7f5bdcc486ea35d1e215c29dcb625cce..0000000000000000000000000000000000000000
--- a/public/ci/build-unknown.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg>
\ No newline at end of file
diff --git a/qa/.gitignore b/qa/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3fec32c842751033d92c8967eba40c3911333a78
--- /dev/null
+++ b/qa/.gitignore
@@ -0,0 +1 @@
+tmp/
diff --git a/qa/.rspec b/qa/.rspec
new file mode 100644
index 0000000000000000000000000000000000000000..b83d9b7aa658619d728a107d5d9f75ae63faff69
--- /dev/null
+++ b/qa/.rspec
@@ -0,0 +1,3 @@
+--color
+--format documentation
+--require spec_helper
diff --git a/qa/Dockerfile b/qa/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..72c82503542ad951325a141e713cebf9dfaddee7
--- /dev/null
+++ b/qa/Dockerfile
@@ -0,0 +1,17 @@
+FROM ruby:2.3
+LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
+
+RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
+    apt-get update && apt-get install -y --force-yes \
+      libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
+    apt-get clean
+
+WORKDIR /home/qa
+
+COPY ./Gemfile* ./
+
+RUN bundle install
+
+COPY ./ ./
+
+ENTRYPOINT ["bin/test"]
diff --git a/qa/Gemfile b/qa/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..6bfe25ba4371b6372bb7743b47f7269fdc923c80
--- /dev/null
+++ b/qa/Gemfile
@@ -0,0 +1,7 @@
+source 'https://rubygems.org'
+
+gem 'capybara', '~> 2.12.1'
+gem 'capybara-screenshot', '~> 1.0.14'
+gem 'capybara-webkit', '~> 1.12.0'
+gem 'rake', '~> 12.0.0'
+gem 'rspec', '~> 3.5'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..6de2abff1985fa60949cd279bcc08f5d6488f7b6
--- /dev/null
+++ b/qa/Gemfile.lock
@@ -0,0 +1,61 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.5.0)
+      public_suffix (~> 2.0, >= 2.0.2)
+    capybara (2.12.1)
+      addressable
+      mime-types (>= 1.16)
+      nokogiri (>= 1.3.3)
+      rack (>= 1.0.0)
+      rack-test (>= 0.5.4)
+      xpath (~> 2.0)
+    capybara-screenshot (1.0.14)
+      capybara (>= 1.0, < 3)
+      launchy
+    capybara-webkit (1.12.0)
+      capybara (>= 2.3.0, < 2.13.0)
+      json
+    diff-lcs (1.3)
+    json (2.0.3)
+    launchy (2.4.3)
+      addressable (~> 2.3)
+    mime-types (3.1)
+      mime-types-data (~> 3.2015)
+    mime-types-data (3.2016.0521)
+    mini_portile2 (2.1.0)
+    nokogiri (1.7.0.1)
+      mini_portile2 (~> 2.1.0)
+    public_suffix (2.0.5)
+    rack (2.0.1)
+    rack-test (0.6.3)
+      rack (>= 1.0)
+    rake (12.0.0)
+    rspec (3.5.0)
+      rspec-core (~> 3.5.0)
+      rspec-expectations (~> 3.5.0)
+      rspec-mocks (~> 3.5.0)
+    rspec-core (3.5.4)
+      rspec-support (~> 3.5.0)
+    rspec-expectations (3.5.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.5.0)
+    rspec-mocks (3.5.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.5.0)
+    rspec-support (3.5.0)
+    xpath (2.0.0)
+      nokogiri (~> 1.3)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  capybara (~> 2.12.1)
+  capybara-screenshot (~> 1.0.14)
+  capybara-webkit (~> 1.12.0)
+  rake (~> 12.0.0)
+  rspec (~> 3.5)
+
+BUNDLED WITH
+   1.14.6
diff --git a/qa/README.md b/qa/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b6b5a76f1d3d8063e0055009840374fd08ee1363
--- /dev/null
+++ b/qa/README.md
@@ -0,0 +1,18 @@
+## Integration tests for GitLab
+
+This directory contains integration tests for GitLab.
+
+It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+
+## What GitLab QA is?
+
+GitLab QA is an integration tests suite for GitLab.
+
+These are black-box and entirely click-driven integration tests you can run
+against any existing instance.
+
+## How does it work?
+
+1. When we release a new version of GitLab, we build a Docker images for it.
+1. Along with GitLab Docker Images we also build and publish GitLab QA images.
+1. GitLab QA project uses these images to execute integration tests.
diff --git a/qa/bin/qa b/qa/bin/qa
new file mode 100755
index 0000000000000000000000000000000000000000..cecdeac14db8fb7b7288de0e33c43533ca5e7304
--- /dev/null
+++ b/qa/bin/qa
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require_relative '../qa'
+
+QA::Scenario
+  .const_get(ARGV.shift)
+  .perform(*ARGV)
diff --git a/qa/bin/test b/qa/bin/test
new file mode 100755
index 0000000000000000000000000000000000000000..997392ad6e4f6a08822fafc26d75a13eeaa2ce72
--- /dev/null
+++ b/qa/bin/test
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+xvfb-run bundle exec bin/qa $@
diff --git a/qa/qa.rb b/qa/qa.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58cf615cc9f894dc8738755ba50e93973b102c3c
--- /dev/null
+++ b/qa/qa.rb
@@ -0,0 +1,81 @@
+$: << File.expand_path(File.dirname(__FILE__))
+
+module QA
+  ##
+  # GitLab QA runtime classes, mostly singletons.
+  #
+  module Runtime
+    autoload :Release, 'qa/runtime/release'
+    autoload :User, 'qa/runtime/user'
+    autoload :Namespace, 'qa/runtime/namespace'
+  end
+
+  ##
+  # GitLab QA Scenarios
+  #
+  module Scenario
+    ##
+    # Support files
+    #
+    autoload :Actable, 'qa/scenario/actable'
+    autoload :Template, 'qa/scenario/template'
+
+    ##
+    # Test scenario entrypoints.
+    #
+    module Test
+      autoload :Instance, 'qa/scenario/test/instance'
+    end
+
+    ##
+    # GitLab instance scenarios.
+    #
+    module Gitlab
+      module Project
+        autoload :Create, 'qa/scenario/gitlab/project/create'
+      end
+    end
+  end
+
+  ##
+  # Classes describing structure of GitLab, pages, menus etc.
+  #
+  # Needed to execute click-driven-only black-box tests.
+  #
+  module Page
+    autoload :Base, 'qa/page/base'
+
+    module Main
+      autoload :Entry, 'qa/page/main/entry'
+      autoload :Menu, 'qa/page/main/menu'
+      autoload :Groups, 'qa/page/main/groups'
+      autoload :Projects, 'qa/page/main/projects'
+    end
+
+    module Project
+      autoload :New, 'qa/page/project/new'
+      autoload :Show, 'qa/page/project/show'
+    end
+
+    module Admin
+      autoload :Menu, 'qa/page/admin/menu'
+    end
+  end
+
+  ##
+  # Classes describing operations on Git repositories.
+  #
+  module Git
+    autoload :Repository, 'qa/git/repository'
+  end
+
+  ##
+  # Classes that make it possible to execute features tests.
+  #
+  module Specs
+    autoload :Config, 'qa/specs/config'
+    autoload :Runner, 'qa/specs/runner'
+  end
+end
+
+QA::Runtime::Release.extend_autoloads!
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d1601dfa487faa56db91b5d61b499537d479620
--- /dev/null
+++ b/qa/qa/ce/strategy.rb
@@ -0,0 +1,15 @@
+module QA
+  module CE
+    module Strategy
+      extend self
+
+      def extend_autoloads!
+        # noop
+      end
+
+      def perform_before_hooks
+        # noop
+      end
+    end
+  end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9e199000d6b05a666facddd6a684cc91d957a1f
--- /dev/null
+++ b/qa/qa/git/repository.rb
@@ -0,0 +1,71 @@
+require 'uri'
+
+module QA
+  module Git
+    class Repository
+      include Scenario::Actable
+
+      def self.perform(*args)
+        Dir.mktmpdir do |dir|
+          Dir.chdir(dir) { super }
+        end
+      end
+
+      def location=(address)
+        @location = address
+        @uri = URI(address)
+      end
+
+      def username=(name)
+        @username = name
+        @uri.user = name
+      end
+
+      def password=(pass)
+        @password = pass
+        @uri.password = pass
+      end
+
+      def use_default_credentials
+        self.username = Runtime::User.name
+        self.password = Runtime::User.password
+      end
+
+      def clone(opts = '')
+        `git clone #{opts} #{@uri.to_s} ./`
+      end
+
+      def shallow_clone
+        clone('--depth 1')
+      end
+
+      def configure_identity(name, email)
+        `git config user.name #{name}`
+        `git config user.email #{email}`
+      end
+
+      def commit_file(name, contents, message)
+        add_file(name, contents)
+        commit(message)
+      end
+
+      def add_file(name, contents)
+        File.write(name, contents)
+
+        `git add #{name}`
+      end
+
+      def commit(message)
+        `git commit -m "#{message}"`
+      end
+
+      def push_changes(branch = 'master')
+        `git push #{@uri.to_s} #{branch}`
+      end
+
+      def commits
+        `git log --oneline`.split("\n")
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b01a4e10f93fd183f7a9f42587382055c6f3bf12
--- /dev/null
+++ b/qa/qa/page/admin/menu.rb
@@ -0,0 +1,19 @@
+module QA
+  module Page
+    module Admin
+      class Menu < Page::Base
+        def go_to_license
+          within_middle_menu { click_link 'License' }
+        end
+
+        private
+
+        def within_middle_menu
+          page.within('.nav-control') do
+            yield
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d55326c52621191aeb5621a11256693f8a19a350
--- /dev/null
+++ b/qa/qa/page/base.rb
@@ -0,0 +1,12 @@
+module QA
+  module Page
+    class Base
+      include Capybara::DSL
+      include Scenario::Actable
+
+      def refresh
+        visit current_path
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9810beeb296f19b60bc9bab8bf6ac563c2f190d
--- /dev/null
+++ b/qa/qa/page/main/entry.rb
@@ -0,0 +1,32 @@
+module QA
+  module Page
+    module Main
+      class Entry < Page::Base
+        def initialize
+          visit('/')
+
+          # This resolves cold boot / background tasks problems
+          #
+          start = Time.now
+
+          while Time.now - start < 240
+            break if page.has_css?('.application', wait: 10)
+            refresh
+          end
+        end
+
+        def sign_in_using_credentials
+          if page.has_content?('Change your password')
+            fill_in :user_password, with: Runtime::User.password
+            fill_in :user_password_confirmation, with: Runtime::User.password
+            click_button 'Change your password'
+          end
+
+          fill_in :user_login, with: Runtime::User.name
+          fill_in :user_password, with: Runtime::User.password
+          click_button 'Sign in'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84597719a84b87148a7de5314682565c693fe751
--- /dev/null
+++ b/qa/qa/page/main/groups.rb
@@ -0,0 +1,20 @@
+module QA
+  module Page
+    module Main
+      class Groups < Page::Base
+        def prepare_test_namespace
+          return if page.has_content?(Runtime::Namespace.name)
+
+          click_on 'New Group'
+
+          fill_in 'group_path', with: Runtime::Namespace.name
+          fill_in 'group_description',
+                  with: "QA test run at #{Runtime::Namespace.time}"
+          choose 'Private'
+
+          click_button 'Create group'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
new file mode 100644
index 0000000000000000000000000000000000000000..45db7a92fa42cafe3a57b2bb60429f3843e93d4a
--- /dev/null
+++ b/qa/qa/page/main/menu.rb
@@ -0,0 +1,46 @@
+module QA
+  module Page
+    module Main
+      class Menu < Page::Base
+        def go_to_groups
+          within_global_menu { click_link 'Groups' }
+        end
+
+        def go_to_projects
+          within_global_menu { click_link 'Projects' }
+        end
+
+        def go_to_admin_area
+          within_user_menu { click_link 'Admin Area' }
+        end
+
+        def sign_out
+          within_user_menu do
+            find('.header-user-dropdown-toggle').click
+            click_link('Sign out')
+          end
+        end
+
+        def has_personal_area?
+          page.has_selector?('.header-user-dropdown-toggle')
+        end
+
+        private
+
+        def within_global_menu
+          find('.global-dropdown-toggle').click
+
+          page.within('.global-dropdown-menu') do
+            yield
+          end
+        end
+
+        def within_user_menu
+          page.within('.navbar-nav') do
+            yield
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28d3a4240221da07fd5339cf10f9a067d49f4038
--- /dev/null
+++ b/qa/qa/page/main/projects.rb
@@ -0,0 +1,16 @@
+module QA
+  module Page
+    module Main
+      class Projects < Page::Base
+        def go_to_new_project
+          ##
+          # There are 'New Project' and 'New project' buttons on the projects
+          # page, so we can't use `click_on`.
+          #
+          button = find('a', text: /^new project$/i)
+          button.click
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b31bec27b590747e44f0db70bec6409ed369355b
--- /dev/null
+++ b/qa/qa/page/project/new.rb
@@ -0,0 +1,24 @@
+module QA
+  module Page
+    module Project
+      class New < Page::Base
+        def choose_test_namespace
+          find('#s2id_project_namespace_id').click
+          find('.select2-result-label', text: Runtime::Namespace.name).click
+        end
+
+        def choose_name(name)
+          fill_in 'project_path', with: name
+        end
+
+        def add_description(description)
+          fill_in 'project_description', with: description
+        end
+
+        def create_new_project
+          click_on 'Create project'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
new file mode 100644
index 0000000000000000000000000000000000000000..56a270d8fcc80beaf0839ec4452f961cf100cbd8
--- /dev/null
+++ b/qa/qa/page/project/show.rb
@@ -0,0 +1,23 @@
+module QA
+  module Page
+    module Project
+      class Show < Page::Base
+        def choose_repository_clone_http
+          find('#clone-dropdown').click
+
+          page.within('#clone-dropdown') do
+            find('span', text: 'HTTP').click
+          end
+        end
+
+        def repository_location
+          find('#project_clone').value
+        end
+
+        def wait_for_push
+          sleep 5
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4910b63a14375d17f5748d901a8515c2e4a6121
--- /dev/null
+++ b/qa/qa/runtime/namespace.rb
@@ -0,0 +1,15 @@
+module QA
+  module Runtime
+    module Namespace
+      extend self
+
+      def time
+        @time ||= Time.now
+      end
+
+      def name
+        'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f83a77364570f95a350f115c28726073e1f2163
--- /dev/null
+++ b/qa/qa/runtime/release.rb
@@ -0,0 +1,28 @@
+module QA
+  module Runtime
+    ##
+    # Class that is responsible for plugging CE/EE extensions in, depending on
+    # existence of EE module.
+    #
+    # We need that to reduce the probability of conflicts when merging
+    # CE to EE.
+    #
+    class Release
+      def initialize
+        require "qa/#{version.downcase}/strategy"
+      end
+
+      def version
+        @version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE
+      end
+
+      def strategy
+        QA.const_get("QA::#{version}::Strategy")
+      end
+
+      def self.method_missing(name, *args)
+        self.new.strategy.public_send(name, *args)
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..12ceda015f038f43227cb558827e8c3ab65d321f
--- /dev/null
+++ b/qa/qa/runtime/user.rb
@@ -0,0 +1,15 @@
+module QA
+  module Runtime
+    module User
+      extend self
+
+      def name
+        ENV['GITLAB_USERNAME'] || 'root'
+      end
+
+      def password
+        ENV['GITLAB_PASSWORD'] || 'test1234'
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6cdbd24780e4e6be84854c76504cffe6936e5ad3
--- /dev/null
+++ b/qa/qa/scenario/actable.rb
@@ -0,0 +1,23 @@
+module QA
+  module Scenario
+    module Actable
+      def act(*args, &block)
+        instance_exec(*args, &block)
+      end
+
+      def self.included(base)
+        base.extend(ClassMethods)
+      end
+
+      module ClassMethods
+        def perform
+          yield new if block_given?
+        end
+
+        def act(*args, &block)
+          new.act(*args, &block)
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38522714e64f45513c06f4ae86887647900ba09b
--- /dev/null
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -0,0 +1,31 @@
+require 'securerandom'
+
+module QA
+  module Scenario
+    module Gitlab
+      module Project
+        class Create < Scenario::Template
+          attr_writer :description
+
+          def name=(name)
+            @name = "#{name}-#{SecureRandom.hex(8)}"
+          end
+
+          def perform
+            Page::Main::Menu.act { go_to_groups }
+            Page::Main::Groups.act { prepare_test_namespace }
+            Page::Main::Menu.act { go_to_projects }
+            Page::Main::Projects.act { go_to_new_project }
+
+            Page::Project::New.perform do |page|
+              page.choose_test_namespace
+              page.choose_name(@name)
+              page.add_description(@description)
+              page.create_new_project
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..341998af1609f15a9fa34eac4eedc607fdea3772
--- /dev/null
+++ b/qa/qa/scenario/template.rb
@@ -0,0 +1,16 @@
+module QA
+  module Scenario
+    class Template
+      def self.perform(*args)
+        new.tap do |scenario|
+          yield scenario if block_given?
+          return scenario.perform(*args)
+        end
+      end
+
+      def perform(*_args)
+        raise NotImplementedError
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
new file mode 100644
index 0000000000000000000000000000000000000000..689292bc60b8cb5277d4d4bfd474edb8f1587b8d
--- /dev/null
+++ b/qa/qa/scenario/test/instance.rb
@@ -0,0 +1,26 @@
+module QA
+  module Scenario
+    module Test
+      ##
+      # Run test suite against any GitLab instance,
+      # including staging and on-premises installation.
+      #
+      class Instance < Scenario::Template
+        def perform(address, *files)
+          Specs::Config.perform do |specs|
+            specs.address = address
+          end
+
+          ##
+          # Perform before hooks, which are different for CE and EE
+          #
+          Runtime::Release.perform_before_hooks
+
+          Specs::Runner.perform do |specs|
+            specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d72187fcd34ff750f417e51d752a0949207a1e88
--- /dev/null
+++ b/qa/qa/specs/config.rb
@@ -0,0 +1,78 @@
+require 'rspec/core'
+require 'capybara/rspec'
+require 'capybara-webkit'
+require 'capybara-screenshot/rspec'
+
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+
+module QA
+  module Specs
+    class Config < Scenario::Template
+      attr_writer :address
+
+      def initialize
+        @address = ENV['GITLAB_URL']
+      end
+
+      def perform
+        raise 'Please configure GitLab address!' unless @address
+
+        configure_rspec!
+        configure_capybara!
+        configure_webkit!
+      end
+
+      def configure_rspec!
+        RSpec.configure do |config|
+          config.expect_with :rspec do |expectations|
+            # This option will default to `true` in RSpec 4. It makes the `description`
+            # and `failure_message` of custom matchers include text for helper methods
+            # defined using `chain`.
+            expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+          end
+
+          config.mock_with :rspec do |mocks|
+            # Prevents you from mocking or stubbing a method that does not exist on
+            # a real object. This is generally recommended, and will default to
+            # `true` in RSpec 4.
+            mocks.verify_partial_doubles = true
+          end
+
+          # Run specs in random order to surface order dependencies.
+          config.order = :random
+          Kernel.srand config.seed
+
+          config.before(:all) do
+            page.current_window.resize_to(1200, 1800)
+          end
+
+          config.formatter = :documentation
+          config.color = true
+        end
+      end
+
+      def configure_capybara!
+        Capybara.configure do |config|
+          config.app_host = @address
+          config.default_driver = :webkit
+          config.javascript_driver = :webkit
+          config.default_max_wait_time = 4
+
+          # https://github.com/mattheworiordan/capybara-screenshot/issues/164
+          config.save_path = 'tmp'
+        end
+      end
+
+      def configure_webkit!
+        Capybara::Webkit.configure do |config|
+          config.allow_url(@address)
+          config.block_unknown_urls
+        end
+      rescue RuntimeError # rubocop:disable Lint/HandleExceptions
+        # TODO, Webkit is already configured, this make this
+        # configuration step idempotent, should be improved.
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e1ae6efa47da70dc528e7fa4afc213b18c1e7ca
--- /dev/null
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -0,0 +1,14 @@
+module QA
+  feature 'standard root login' do
+    scenario 'user logs in using credentials' do
+      Page::Main::Entry.act { sign_in_using_credentials }
+
+      # TODO, since `Signed in successfully` message was removed
+      # this is the only way to tell if user is signed in correctly.
+      #
+      Page::Main::Menu.perform do |menu|
+        expect(menu).to have_personal_area
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..610492b9717e79d9f1bdb981a0c43b513d3cd355
--- /dev/null
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -0,0 +1,19 @@
+module QA
+  feature 'create a new project' do
+    scenario 'user creates a new project' do
+      Page::Main::Entry.act { sign_in_using_credentials }
+
+      Scenario::Gitlab::Project::Create.perform do |project|
+        project.name = 'awesome-project'
+        project.description = 'create awesome project test'
+      end
+
+      expect(page).to have_content(
+        /Project \S?awesome-project\S+ was successfully created/
+      )
+
+      expect(page).to have_content('create awesome project test')
+      expect(page).to have_content('The repository for this project is empty')
+    end
+  end
+end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..521bd955857e8c2a14ab5a6134c9d5e82b92cada
--- /dev/null
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -0,0 +1,57 @@
+module QA
+  feature 'clone code from the repository' do
+    context 'with regular account over http' do
+      given(:location) do
+        Page::Project::Show.act do
+          choose_repository_clone_http
+          repository_location
+        end
+      end
+
+      before do
+        Page::Main::Entry.act { sign_in_using_credentials }
+
+        Scenario::Gitlab::Project::Create.perform do |scenario|
+          scenario.name = 'project-with-code'
+          scenario.description = 'project for git clone tests'
+        end
+
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act do
+            clone
+            configure_identity('GitLab QA', 'root@gitlab.com')
+            commit_file('test.rb', 'class Test; end', 'Add Test class')
+            commit_file('README.md', '# Test', 'Add Readme')
+            push_changes
+          end
+        end
+      end
+
+      scenario 'user performs a deep clone' do
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act { clone }
+
+          expect(repository.commits.size).to eq 2
+        end
+      end
+
+      scenario 'user performs a shallow clone' do
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act { shallow_clone }
+
+          expect(repository.commits.size).to eq 1
+          expect(repository.commits.first).to include 'Add Readme'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fe45d63d37c8a83822827352be755aaa29164a1
--- /dev/null
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -0,0 +1,39 @@
+module QA
+  feature 'push code to repository' do
+    context 'with regular account over http' do
+      scenario 'user pushes code to the repository' do
+        Page::Main::Entry.act { sign_in_using_credentials }
+
+        Scenario::Gitlab::Project::Create.perform do |scenario|
+          scenario.name = 'project_with_code'
+          scenario.description = 'project with repository'
+        end
+
+        Git::Repository.perform do |repository|
+          repository.location = Page::Project::Show.act do
+            choose_repository_clone_http
+            repository_location
+          end
+
+          repository.use_default_credentials
+
+          repository.act do
+            clone
+            configure_identity('GitLab QA', 'root@gitlab.com')
+            add_file('README.md', '# This is test project')
+            commit('Add README.md')
+            push_changes
+          end
+        end
+
+        Page::Project::Show.act do
+          wait_for_push
+          refresh
+        end
+
+        expect(page).to have_content('README.md')
+        expect(page).to have_content('This is test project')
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83ae15d099522d9cde8c50d69fa7bbc3dd5b7f67
--- /dev/null
+++ b/qa/qa/specs/runner.rb
@@ -0,0 +1,15 @@
+require 'rspec/core'
+
+module QA
+  module Specs
+    class Runner
+      include Scenario::Actable
+
+      def rspec(*args)
+        RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+          abort if status.nonzero?
+        end
+      end
+    end
+  end
+end
diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6b5a8dc315e66d9654aee25166dab32af61ec08
--- /dev/null
+++ b/qa/spec/runtime/release_spec.rb
@@ -0,0 +1,50 @@
+describe QA::Runtime::Release do
+  context 'when release version has extension strategy' do
+    let(:strategy) { spy('strategy') }
+
+    before do
+      stub_const('QA::CE::Strategy', strategy)
+      stub_const('QA::EE::Strategy', strategy)
+    end
+
+    describe '#version' do
+      it 'return either CE or EE version' do
+        expect(subject.version).to eq(:CE).or eq(:EE)
+      end
+    end
+
+    describe '#strategy' do
+      it 'return the strategy constant' do
+        expect(subject.strategy).to eq strategy
+      end
+    end
+
+    describe 'delegated class methods' do
+      it 'delegates all calls to strategy class' do
+        described_class.some_method(1, 2)
+
+        expect(strategy).to have_received(:some_method)
+          .with(1, 2)
+      end
+    end
+  end
+
+  context 'when release version does not have extension strategy' do
+    before do
+      allow_any_instance_of(described_class)
+        .to receive(:version).and_return('something')
+    end
+
+    describe '#strategy' do
+      it 'raises error' do
+        expect { subject.strategy }.to raise_error(LoadError)
+      end
+    end
+
+    describe 'delegated class methods' do
+      it 'raises error' do
+        expect { described_class.some_method(2, 3) }.to raise_error(LoadError)
+      end
+    end
+  end
+end
diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..422763910e49b842927000476731e7d7669403e0
--- /dev/null
+++ b/qa/spec/scenario/actable_spec.rb
@@ -0,0 +1,47 @@
+describe QA::Scenario::Actable do
+  subject do
+    Class.new do
+      include QA::Scenario::Actable
+
+      attr_accessor :something
+
+      def do_something(arg = nil)
+        "some#{arg}"
+      end
+    end
+  end
+
+  describe '.act' do
+    it 'provides means to run steps' do
+      result = subject.act { do_something }
+
+      expect(result).to eq 'some'
+    end
+
+    it 'supports passing variables' do
+      result = subject.act('thing') do |variable|
+        do_something(variable)
+      end
+
+      expect(result).to eq 'something'
+    end
+
+    it 'returns value from the last method' do
+      result = subject.act { 'test' }
+
+      expect(result).to eq 'test'
+    end
+  end
+
+  describe '.perform' do
+    it 'makes it possible to pass binding' do
+      variable = 'something'
+
+      result = subject.perform do |object|
+        object.something = variable
+      end
+
+      expect(result).to eq 'something'
+    end
+  end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c07a32346734b883388acd66fd0aff64a301d5a2
--- /dev/null
+++ b/qa/spec/spec_helper.rb
@@ -0,0 +1,19 @@
+require_relative '../qa'
+
+RSpec.configure do |config|
+  config.expect_with :rspec do |expectations|
+    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+  end
+
+  config.mock_with :rspec do |mocks|
+    mocks.verify_partial_doubles = true
+  end
+
+  config.shared_context_metadata_behavior = :apply_to_host_groups
+  config.disable_monkey_patching!
+  config.expose_dsl_globally = true
+  config.warnings = true
+  config.profile_examples = 10
+  config.order = :random
+  Kernel.srand config.seed
+end
diff --git a/rubocop/cop/custom_error_class.rb b/rubocop/cop/custom_error_class.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38d93acfe8818923b45ffa8c305ab1c8b14dd526
--- /dev/null
+++ b/rubocop/cop/custom_error_class.rb
@@ -0,0 +1,64 @@
+module RuboCop
+  module Cop
+    # This cop makes sure that custom error classes, when empty, are declared
+    # with Class.new.
+    #
+    # @example
+    #   # bad
+    #   class FooError < StandardError
+    #   end
+    #
+    #   # okish
+    #   class FooError < StandardError; end
+    #
+    #   # good
+    #   FooError = Class.new(StandardError)
+    class CustomErrorClass < RuboCop::Cop::Cop
+      MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze
+
+      def on_class(node)
+        _klass, parent, body = node.children
+
+        return if body
+
+        parent_klass = class_name_from_node(parent)
+
+        return unless parent_klass && parent_klass.to_s.end_with?('Error')
+
+        add_offense(node, :expression)
+      end
+
+      def autocorrect(node)
+        klass, parent, _body = node.children
+        replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})"
+
+        lambda do |corrector|
+          corrector.replace(node.source_range, replacement)
+        end
+      end
+
+      private
+
+      # The nested constant `Foo::Bar::Baz` looks like:
+      #
+      #   s(:const,
+      #     s(:const,
+      #       s(:const, nil, :Foo), :Bar), :Baz)
+      #
+      # So recurse through that to get the name as written in the source.
+      #
+      def class_name_from_node(node, suffix = nil)
+        return unless node&.type == :const
+
+        name = node.children[1].to_s
+        name = "#{name}::#{suffix}" if suffix
+
+        if node.children[0]
+          class_name_from_node(node.children[0], name)
+        else
+          name
+        end
+      end
+    end
+  end
+end
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
index c199f6acab2ca8c007f8cec942ff59775f50fec6..e157d8e0791cc424cf534e1f1b52dcf6bbeec125 100644
--- a/rubocop/cop/gem_fetcher.rb
+++ b/rubocop/cop/gem_fetcher.rb
@@ -4,9 +4,9 @@ module RuboCop
     # `Gemfile` in order to avoid additional points of failure beyond
     # rubygems.org.
     class GemFetcher < RuboCop::Cop::Cop
-      MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+      MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'.freeze
 
-      GIT_KEYS = [:git, :github]
+      GIT_KEYS = [:git, :github].freeze
 
       def on_send(node)
         return unless gemfile?(node)
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
index 1490fcdd814e7e77d48e30ccd506143dce7924cc..d2cf36c454a92dad6f280de99cffba4b6e59239e 100644
--- a/rubocop/cop/migration/add_column.rb
+++ b/rubocop/cop/migration/add_column.rb
@@ -8,10 +8,10 @@ module RuboCop
       class AddColumn < RuboCop::Cop::Cop
         include MigrationHelpers
 
-        WHITELISTED_TABLES = [:application_settings]
+        WHITELISTED_TABLES = [:application_settings].freeze
 
         MSG = '`add_column` with a default value requires downtime, ' \
-          'use `add_column_with_default` instead'
+          'use `add_column_with_default` instead'.freeze
 
         def on_send(node)
           return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
index 747d7caf1efeb0bcf7f37cee13fd10f8032b4b74..54a920d4b49341f155857b44a7004851c31e3e05 100644
--- a/rubocop/cop/migration/add_column_with_default.rb
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -9,7 +9,7 @@ module RuboCop
         include MigrationHelpers
 
         MSG = '`add_column_with_default` is not reversible so you must manually define ' \
-          'the `up` and `down` methods in your migration class, using `remove_column` in `down`'
+          'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
 
         def on_send(node)
           return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
index e40a7087a4710dfead0bb00cc2a4ee342b2db6c7..d1fc94d55be6154a1313ccb37523516fea8f4092 100644
--- a/rubocop/cop/migration/add_concurrent_foreign_key.rb
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -8,7 +8,7 @@ module RuboCop
       class AddConcurrentForeignKey < RuboCop::Cop::Cop
         include MigrationHelpers
 
-        MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'
+        MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
 
         def on_send(node)
           return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..332fb7dcbd79689d171f1c569418215dd35bb272
--- /dev/null
+++ b/rubocop/cop/migration/add_concurrent_index.rb
@@ -0,0 +1,34 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+  module Cop
+    module Migration
+      # Cop that checks if `add_concurrent_index` is used with `up`/`down` methods
+      # and not `change`.
+      class AddConcurrentIndex < RuboCop::Cop::Cop
+        include MigrationHelpers
+
+        MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
+          'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze
+
+        def on_send(node)
+          return unless in_migration?(node)
+
+          name = node.children[1]
+
+          return unless name == :add_concurrent_index
+
+          node.each_ancestor(:def) do |def_node|
+            next unless method_name(def_node) == :change
+
+            add_offense(def_node, :name)
+          end
+        end
+
+        def method_name(node)
+          node.children.first
+        end
+      end
+    end
+  end
+end
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
index 5e6766f6994545496ae8b60982d05860b9086239..fa21a0d6555e2631691812fcd93d9dbc0d131484 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -7,7 +7,7 @@ module RuboCop
       class AddIndex < RuboCop::Cop::Cop
         include MigrationHelpers
 
-        MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'
+        MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'.freeze
 
         def on_def(node)
           return unless in_migration?(node)
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index d4266d0deae6df23bb857cbe90acb3053dcc52d7..a50a522cf9ded8270543979b180e4512ab43e1c1 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,5 +1,7 @@
+require_relative 'cop/custom_error_class'
 require_relative 'cop/gem_fetcher'
 require_relative 'cop/migration/add_column'
 require_relative 'cop/migration/add_column_with_default'
 require_relative 'cop/migration/add_concurrent_foreign_key'
+require_relative 'cop/migration/add_concurrent_index'
 require_relative 'cop/migration/add_index'
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 0b8ff006d22846114abd19af5882fc6adfd389b8..092048a62599afa31298ac3663f5a4b11fc1725a 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,20 +1,36 @@
 require 'spec_helper'
 
 describe 'mail_room.yml' do
-  let(:config_path)   { 'config/mail_room.yml' }
-  let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
-  before(:each) { clear_raw_config }
-  after(:each) { clear_raw_config }
+  include StubENV
 
-  context 'when incoming email is disabled' do
-    before do
-      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s
-      Gitlab::MailRoom.reset_config!
-    end
+  let(:mailroom_config_path) { 'config/mail_room.yml' }
+  let(:gitlab_config_path) { 'config/mail_room.yml' }
+  let(:redis_config_path) { 'config/resque.yml' }
 
-    after do
-      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
-    end
+  let(:configuration) do
+    vars = {
+      'MAIL_ROOM_GITLAB_CONFIG_FILE' => absolute_path(gitlab_config_path),
+      'GITLAB_REDIS_CONFIG_FILE' => absolute_path(redis_config_path)
+    }
+    cmd = "puts ERB.new(File.read(#{absolute_path(mailroom_config_path).inspect})).result"
+
+    output, status = Gitlab::Popen.popen(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars)
+    raise "Error interpreting #{mailroom_config_path}: #{output}" unless status.zero?
+
+    YAML.load(output)
+  end
+
+  before(:each) do
+    stub_env('GITLAB_REDIS_CONFIG_FILE', absolute_path(redis_config_path))
+    clear_redis_raw_config
+  end
+
+  after(:each) do
+    clear_redis_raw_config
+  end
+
+  context 'when incoming email is disabled' do
+    let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_disabled.yml' }
 
     it 'contains no configuration' do
       expect(configuration[:mailboxes]).to be_nil
@@ -22,21 +38,12 @@ describe 'mail_room.yml' do
   end
 
   context 'when incoming email is enabled' do
-    let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
-    let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
-
-    before do
-      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s
-      Gitlab::MailRoom.reset_config!
-    end
+    let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
+    let(:redis_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
 
-    after do
-      ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
-    end
+    let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
 
     it 'contains the intended configuration' do
-      stub_const('Gitlab::Redis::CONFIG_FILE', redis_config)
-
       expect(configuration[:mailboxes].length).to eq(1)
       mailbox = configuration[:mailboxes].first
 
@@ -66,9 +73,13 @@ describe 'mail_room.yml' do
     end
   end
 
-  def clear_raw_config
+  def clear_redis_raw_config
     Gitlab::Redis.remove_instance_variable(:@_raw_config)
   rescue NameError
     # raised if @_raw_config was not set; ignore
   end
+
+  def absolute_path(path)
+    Rails.root.join(path).to_s
+  end
 end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84a1ce773a1612f740fe12639a4224d2e9e331e3
--- /dev/null
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::ApplicationSettingsController do
+  include StubENV
+
+  let(:admin) { create(:admin) }
+
+  before do
+    sign_in(admin)
+    stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+  end
+
+  describe 'PATCH #update' do
+    it 'updates the default_project_visibility for string value' do
+      patch :update, application_setting: { default_project_visibility: "20" }
+
+      expect(response).to redirect_to(admin_application_settings_path)
+      expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC
+    end
+
+    it 'falls back to default with default_project_visibility setting is omitted' do
+      patch :update, application_setting: {}
+
+      expect(response).to redirect_to(admin_application_settings_path)
+      expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE
+    end
+  end
+end
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e311b8a63b236c14384d4430121579a459d89456
--- /dev/null
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Admin::ApplicationsController do
+  let(:admin) { create(:admin) }
+  let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
+
+  before do
+    sign_in(admin)
+  end
+
+  describe 'GET #new' do
+    it 'renders the application form' do
+      get :new
+
+      expect(response).to render_template :new
+      expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+    end
+  end
+
+  describe 'GET #edit' do
+    it 'renders the application form' do
+      get :edit, id: application.id
+
+      expect(response).to render_template :edit
+      expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+    end
+  end
+
+  describe 'POST #create' do
+    it 'creates the application' do
+      expect do
+        post :create, doorkeeper_application: attributes_for(:application)
+      end.to change { Doorkeeper::Application.count }.by(1)
+
+      application = Doorkeeper::Application.last
+
+      expect(response).to redirect_to(admin_application_path(application))
+    end
+
+    it 'renders the application form on errors' do
+      expect do
+        post :create, doorkeeper_application: attributes_for(:application).merge(redirect_uri: nil)
+      end.not_to change { Doorkeeper::Application.count }
+
+      expect(response).to render_template :new
+      expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+    end
+  end
+
+  describe 'PATCH #update' do
+    it 'updates the application' do
+      patch :update, id: application.id, doorkeeper_application: { redirect_uri: 'http://example.com/' }
+
+      expect(response).to redirect_to(admin_application_path(application))
+      expect(application.reload.redirect_uri).to eq 'http://example.com/'
+    end
+
+    it 'renders the application form on errors' do
+      patch :update, id: application.id, doorkeeper_application: { redirect_uri: nil }
+
+      expect(response).to render_template :edit
+      expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+    end
+  end
+end
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index 2fcb4a6a528db118be19a0f3c35ae62aa2a66eeb..44e011fd3a85b5cbb0948c55a279d34617f2b65e 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -19,8 +19,8 @@ describe Projects::BlobController do
 
     before do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: id)
     end
 
@@ -50,8 +50,8 @@ describe Projects::BlobController do
 
     before do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: id)
       controller.instance_variable_set(:@blob, nil)
     end
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
deleted file mode 100644
index 86f01f437a2b071adcc0036239fbb0ebdc214e63..0000000000000000000000000000000000000000
--- a/spec/controllers/ci/projects_controller_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProjectsController do
-  let(:visibility) { :public }
-  let!(:project) { create(:empty_project, visibility, ci_id: 1) }
-  let(:ci_id) { project.ci_id }
-
-  describe '#index' do
-    context 'user signed in' do
-      before do
-        sign_in(create(:user))
-        get(:index)
-      end
-
-      it 'redirects to /' do
-        expect(response).to redirect_to(root_path)
-      end
-    end
-
-    context 'user not signed in' do
-      before { get(:index) }
-
-      it 'redirects to sign in page' do
-        expect(response).to redirect_to(new_user_session_path)
-      end
-    end
-  end
-
-  ##
-  # Specs for *deprecated* CI badge
-  #
-  describe '#badge' do
-    shared_examples 'badge provider' do
-      it 'shows badge' do
-        expect(response.status).to eq 200
-        expect(response.headers)
-          .to include('Content-Type' => 'image/svg+xml')
-      end
-    end
-
-    context 'user not signed in' do
-      before { get(:badge, id: ci_id) }
-
-      context 'project has no ci_id reference' do
-        let(:ci_id) { 123 }
-
-        it 'returns 404' do
-          expect(response.status).to eq 404
-        end
-      end
-
-      context 'project is public' do
-        let(:visibility) { :public }
-        it_behaves_like 'badge provider'
-      end
-
-      context 'project is private' do
-        let(:visibility) { :private }
-        it_behaves_like 'badge provider'
-      end
-    end
-
-    context 'user signed in' do
-      let(:user) { create(:user) }
-      before { sign_in(user) }
-      before { get(:badge, id: ci_id) }
-
-      context 'private is internal' do
-        let(:visibility) { :internal }
-        it_behaves_like 'badge provider'
-      end
-    end
-  end
-end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 7072bd5e87cf96a548f8fbd734e0c58698f2acf9..71a4a2c43c78b2e6cc63f9df07ba3b3447bd5492 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -49,4 +49,18 @@ describe Dashboard::TodosController do
       expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
     end
   end
+
+  describe 'PATCH #bulk_restore' do
+    let(:todos) { create_list(:todo, 2, :done, user: user, project: project, author: author) }
+
+    it 'restores the todos to pending state' do
+      patch :bulk_restore, ids: todos.map(&:id)
+
+      todos.each do |todo|
+        expect(todo.reload).to be_pending
+      end
+      expect(response).to have_http_status(200)
+      expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
+    end
+  end
 end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index cfe18dd4b6c86c859bee9628aff0eb4cc5fab8b1..58c16cc57e6acd1fb64d77338160c40d4271a70c 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -64,8 +64,8 @@ describe HealthCheckController do
 
     context 'when a service is down and an access token is provided' do
       before do
-        allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire')
-        allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire')
+        allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire')
+        allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire')
       end
 
       it 'supports passing the token in the header' do
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index f7219690722e7422cb7eeb41cf65a16c2f92bfb9..61e4fae46fbfbc32eee58a8d2b93baafbf6d21e7 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -3,16 +3,6 @@ require 'spec_helper'
 describe Profiles::KeysController do
   let(:user) { create(:user) }
 
-  describe '#new' do
-    before { sign_in(user) }
-
-    it 'redirects to #index' do
-      get :new
-
-      expect(response).to redirect_to(profile_keys_path)
-    end
-  end
-
   describe "#get_keys" do
     describe "non existant user" do
       it "does not generally work" do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
deleted file mode 100644
index 58caf7999cf26409c8c9f1c2b98f9c44d67bb2d6..0000000000000000000000000000000000000000
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'spec_helper'
-
-describe Profiles::NotificationsController do
-  let(:user) do
-    create(:user) do |user|
-      user.emails.create(email: 'original@example.com')
-      user.emails.create(email: 'new@example.com')
-      user.update(notification_email: 'original@example.com')
-      user.save!
-    end
-  end
-
-  describe 'GET show' do
-    it 'renders' do
-      sign_in(user)
-
-      get :show
-
-      expect(response).to render_template :show
-    end
-  end
-
-  describe 'POST update' do
-    it 'updates only permitted attributes' do
-      sign_in(user)
-
-      put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true }
-
-      user.reload
-      expect(user.notification_email).to eq('new@example.com')
-      expect(user.notified_of_own_activity).to eq(true)
-      expect(user.admin).to eq(false)
-      expect(controller).to set_flash[:notice].to('Notification settings saved')
-    end
-
-    it 'shows an error message if the params are invalid' do
-      sign_in(user)
-
-      put :update, user: { notification_email: '' }
-
-      expect(user.reload.notification_email).to eq('original@example.com')
-      expect(controller).to set_flash[:alert].to('Failed to save new settings')
-    end
-  end
-end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index 45534a3a5875abf205c55a853f753bf66024a5b8..dfed1de2046d6f6f6a0ccdf5d42a8b9936d89e1a 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -2,48 +2,55 @@ require 'spec_helper'
 
 describe Profiles::PersonalAccessTokensController do
   let(:user) { create(:user) }
+  let(:token_attributes) { attributes_for(:personal_access_token) }
+
+  before { sign_in(user) }
 
   describe '#create' do
     def created_token
       PersonalAccessToken.order(:created_at).last
     end
 
-    before { sign_in(user) }
-
-    it "allows creation of a token" do
+    it "allows creation of a token with scopes" do
       name = FFaker::Product.brand
+      scopes = %w[api read_user]
 
-      post :create, personal_access_token: { name: name }
+      post :create, personal_access_token: token_attributes.merge(scopes: scopes, 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(created_token.scopes).to eq(scopes)
       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
+      expires_at = 5.days.from_now.to_date
 
-      post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+      post :create, personal_access_token: token_attributes.merge(expires_at: expires_at)
 
       expect(created_token).not_to be_nil
-      expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
+      expect(created_token.expires_at).to eq(expires_at)
     end
+  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'] }
+  describe '#index' do
+    let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+    let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+    let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
 
-        expect(created_token).not_to be_nil
-        expect(created_token.scopes).to eq(['api', 'read_user'])
-      end
+    before { get :index }
 
-      it "allows creation of a token with no scopes" do
-        post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
+    it "retrieves active personal access tokens" do
+      expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token)
+    end
+
+    it "retrieves inactive personal access tokens" do
+      expect(assigns(:inactive_personal_access_tokens)).to include(inactive_personal_access_token)
+    end
 
-        expect(created_token).not_to be_nil
-        expect(created_token.scopes).to eq([])
-      end
+    it "does not retrieve impersonation personal access tokens" do
+      expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token)
+      expect(assigns(:inactive_personal_access_tokens)).not_to include(impersonation_personal_access_token)
     end
   end
 end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index addc5e7ec33c3fbe51b91214e00d45b5cbdfe146..c086b3863811ad8326538dcadbb19ea1a637cd4c 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::BlameController do
 
     before do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: id)
     end
 
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 7d4636e98d1a4c5a2f01b11a46ee6de7fc8b2d19..ec36a64b415af7107eb6246857ffbc8f561401df 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::BlobController do
     render_views
 
     def do_get(opts = {})
-      params = { namespace_id: project.namespace.to_param,
-                 project_id: project.to_param,
+      params = { namespace_id: project.namespace,
+                 project_id: project,
                  id: 'master/CHANGELOG' }
       get :diff, params.merge(opts)
     end
@@ -40,8 +40,8 @@ describe Projects::BlobController do
   describe 'PUT update' do
     let(:default_params) do
       {
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        namespace_id: project.namespace,
+        project_id: project,
         id: 'master/CHANGELOG',
         target_branch: 'master',
         content: 'Added changes',
@@ -96,8 +96,8 @@ describe Projects::BlobController do
 
       context 'when editing on the fork' do
         before do
-          default_params[:namespace_id] = forked_project.namespace.to_param
-          default_params[:project_id] = forked_project.to_param
+          default_params[:namespace_id] = forked_project.namespace
+          default_params[:project_id] = forked_project
         end
 
         it 'redirects to blob' do
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index ad15e3942a5c1276e28c2294cdd224a562388b40..15667e8d4b11611159fc7f33d0fae5fb6c09f2e8 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -43,6 +43,7 @@ describe Projects::Boards::IssuesController do
 
           expect(response).to match_response_schema('issues')
           expect(parsed_response.length).to eq 2
+          expect(development.issues.map(&:relative_position)).not_to include(nil)
         end
       end
 
@@ -90,7 +91,7 @@ describe Projects::Boards::IssuesController do
 
       params = {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        project_id: project,
         board_id: board.to_param,
         list_id: list.try(:to_param)
       }
@@ -146,7 +147,7 @@ describe Projects::Boards::IssuesController do
       sign_in(user)
 
       post :create, namespace_id: project.namespace.to_param,
-                    project_id: project.to_param,
+                    project_id: project,
                     board_id: board.to_param,
                     list_id: list.to_param,
                     issue: { title: title },
@@ -209,7 +210,7 @@ describe Projects::Boards::IssuesController do
       sign_in(user)
 
       patch :update, namespace_id: project.namespace.to_param,
-                     project_id: project.to_param,
+                     project_id: project,
                      board_id: board.to_param,
                      id: issue.to_param,
                      from_list_id: from_list_id,
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index b3f9f76a50cbc20e0307e0b356db97d2ecb87cb1..432f3c53c9005e3f3da5daf8aa5c023a4f28915b 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -47,7 +47,7 @@ describe Projects::Boards::ListsController do
       sign_in(user)
 
       get :index, namespace_id: project.namespace.to_param,
-                  project_id: project.to_param,
+                  project_id: project,
                   board_id: board.to_param,
                   format: :json
     end
@@ -104,7 +104,7 @@ describe Projects::Boards::ListsController do
       sign_in(user)
 
       post :create, namespace_id: project.namespace.to_param,
-                    project_id: project.to_param,
+                    project_id: project,
                     board_id: board.to_param,
                     list: { label_id: label_id },
                     format: :json
@@ -157,7 +157,7 @@ describe Projects::Boards::ListsController do
       sign_in(user)
 
       patch :update, namespace_id: project.namespace.to_param,
-                     project_id: project.to_param,
+                     project_id: project,
                      board_id: board.to_param,
                      id: list.to_param,
                      list: { position: position },
@@ -200,7 +200,7 @@ describe Projects::Boards::ListsController do
       sign_in(user)
 
       delete :destroy, namespace_id: project.namespace.to_param,
-                       project_id: project.to_param,
+                       project_id: project,
                        board_id: board.to_param,
                        id: list.to_param,
                        format: :json
@@ -244,7 +244,7 @@ describe Projects::Boards::ListsController do
       sign_in(user)
 
       post :generate, namespace_id: project.namespace.to_param,
-                      project_id: project.to_param,
+                      project_id: project,
                       board_id: board.to_param,
                       format: :json
     end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index cc19035740ed50f8ca227b44996b16232cde4433..aed3a45c41360e1ad955a31e081fa8e8230a2289 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -50,8 +50,8 @@ describe Projects::BoardsController do
     end
 
     def list_boards(format: :html)
-      get :index, namespace_id: project.namespace.to_param,
-                  project_id: project.to_param,
+      get :index, namespace_id: project.namespace,
+                  project_id: project,
                   format: format
     end
   end
@@ -100,8 +100,8 @@ describe Projects::BoardsController do
     end
 
     def read_board(board:, format: :html)
-      get :show, namespace_id: project.namespace.to_param,
-                 project_id: project.to_param,
+      get :show, namespace_id: project.namespace,
+                 project_id: project,
                  id: board.to_param,
                  format: format
     end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 9de038767553aa035f92a2b2a27e33613aa4ecde..d20e7368086aadec04067a7279626ff919e8af73 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -22,8 +22,8 @@ describe Projects::BranchesController do
         sign_in(user)
 
         post :create,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           branch_name: branch,
           ref: ref
       end
@@ -68,7 +68,7 @@ describe Projects::BranchesController do
 
     describe "created from the new branch button on issues" do
       let(:branch) { "1-feature-branch" }
-      let!(:issue) { create(:issue, project: project) }
+      let(:issue) { create(:issue, project: project) }
 
       before do
         sign_in(user)
@@ -76,8 +76,8 @@ describe Projects::BranchesController do
 
       it 'redirects' do
         post :create,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           branch_name: branch,
           issue_iid: issue.iid
 
@@ -89,12 +89,49 @@ describe Projects::BranchesController do
         expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
 
         post :create,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           branch_name: branch,
           issue_iid: issue.iid
       end
 
+      context 'repository-less project' do
+        let(:project) { create :empty_project }
+
+        it 'redirects to newly created branch' do
+          result = { status: :success, branch: double(name: branch) }
+
+          expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+          expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+          post :create,
+            namespace_id: project.namespace.to_param,
+            project_id: project.to_param,
+            branch_name: branch,
+            issue_iid: issue.iid
+
+          expect(response).to redirect_to namespace_project_tree_path(project.namespace, project, branch)
+        end
+
+        it 'redirects to autodeploy setup page' do
+          result = { status: :success, branch: double(name: branch) }
+
+          project.services << build(:kubernetes_service)
+
+          expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+          expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+          post :create,
+            namespace_id: project.namespace.to_param,
+            project_id: project.to_param,
+            branch_name: branch,
+            issue_iid: issue.iid
+
+          expect(response.location).to include(namespace_project_new_blob_path(project.namespace, project, branch))
+          expect(response).to have_http_status(302)
+        end
+      end
+
       context 'without issue feature access' do
         before do
           project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
@@ -106,8 +143,8 @@ describe Projects::BranchesController do
           expect(SystemNoteService).not_to receive(:new_issue_branch)
 
           post :create,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            namespace_id: project.namespace,
+            project_id: project,
             branch_name: branch,
             issue_iid: issue.iid
         end
@@ -126,8 +163,8 @@ describe Projects::BranchesController do
       post :destroy,
            format: :html,
            id: 'foo/bar/baz',
-           namespace_id: project.namespace.to_param,
-           project_id: project.to_param
+           namespace_id: project.namespace,
+           project_id: project
 
       expect(response).to have_http_status(303)
     end
@@ -142,8 +179,8 @@ describe Projects::BranchesController do
       post :destroy,
            format: :js,
            id: branch,
-           namespace_id: project.namespace.to_param,
-           project_id: project.to_param
+           namespace_id: project.namespace,
+           project_id: project
     end
 
     context "valid branch name, valid source" do
@@ -173,8 +210,8 @@ describe Projects::BranchesController do
   describe "DELETE destroy_all_merged" do
     def destroy_all_merged
       delete :destroy_all_merged,
-             namespace_id: project.namespace.to_param,
-             project_id: project.to_param
+             namespace_id: project.namespace,
+             project_id: project
     end
 
     context 'when user is allowed to push' do
@@ -207,4 +244,41 @@ describe Projects::BranchesController do
       end
     end
   end
+
+  describe "GET index" do
+    render_views
+
+    before do
+      sign_in(user)
+    end
+
+    context 'when rendering a JSON format' do
+      it 'filters branches by name' do
+        get :index,
+            namespace_id: project.namespace,
+            project_id: project,
+            format: :json,
+            search: 'master'
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(parsed_response.length).to eq 1
+        expect(parsed_response.first).to eq 'master'
+      end
+    end
+
+    context 'show_all = true' do
+      it 'returns all the branches name' do
+        get :index,
+            namespace_id: project.namespace,
+            project_id: project,
+            format: :json,
+            show_all: true
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(parsed_response.length).to eq(project.repository.branches.count)
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index ebd2d0e092b9df5ffbbe04c6d3ee697adfc05739..b223a22ae604a02e1c8ce77d5cb0f018ed469625 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::CommitController do
 
     def go(extra_params = {})
       params = {
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
       }
 
       get :show, params.merge(extra_params)
@@ -125,8 +125,8 @@ describe Projects::CommitController do
 
       it 'renders it' do
         get(:show,
-            namespace_id: fork_project.namespace.to_param,
-            project_id: fork_project.to_param,
+            namespace_id: fork_project.namespace,
+            project_id: fork_project,
             id: commit.id)
 
         expect(response).to be_success
@@ -139,8 +139,8 @@ describe Projects::CommitController do
       commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
 
       get(:branches,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: commit.id)
 
       expect(assigns(:branches)).to include("master", "feature_conflict")
@@ -152,8 +152,8 @@ describe Projects::CommitController do
     context 'when target branch is not provided' do
       it 'renders the 404 page' do
         post(:revert,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            namespace_id: project.namespace,
+            project_id: project,
             id: commit.id)
 
         expect(response).not_to be_success
@@ -164,9 +164,9 @@ describe Projects::CommitController do
     context 'when the revert was successful' do
       it 'redirects to the commits page' do
         post(:revert,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: commit.id)
 
         expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
@@ -177,18 +177,18 @@ describe Projects::CommitController do
     context 'when the revert failed' do
       before do
         post(:revert,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: commit.id)
       end
 
       it 'redirects to the commit page' do
         # Reverting a commit that has been already reverted.
         post(:revert,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: commit.id)
 
         expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
@@ -201,8 +201,8 @@ describe Projects::CommitController do
     context 'when target branch is not provided' do
       it 'renders the 404 page' do
         post(:cherry_pick,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            namespace_id: project.namespace,
+            project_id: project,
             id: master_pickable_commit.id)
 
         expect(response).not_to be_success
@@ -213,9 +213,9 @@ describe Projects::CommitController do
     context 'when the cherry-pick was successful' do
       it 'redirects to the commits page' do
         post(:cherry_pick,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: master_pickable_commit.id)
 
         expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
@@ -226,18 +226,18 @@ describe Projects::CommitController do
     context 'when the cherry_pick failed' do
       before do
         post(:cherry_pick,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: master_pickable_commit.id)
       end
 
       it 'redirects to the commit page' do
         # Cherry-picking a commit that has been already cherry-picked.
         post(:cherry_pick,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            target_branch: 'master',
+            namespace_id: project.namespace,
+            project_id: project,
+            start_branch: 'master',
             id: master_pickable_commit.id)
 
         expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
@@ -249,8 +249,8 @@ describe Projects::CommitController do
   describe 'GET diff_for_path' do
     def diff_for_path(extra_params = {})
       params = {
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
       }
 
       get :diff_for_path, params.merge(extra_params)
@@ -313,8 +313,8 @@ describe Projects::CommitController do
   describe 'GET pipelines' do
     def get_pipelines(extra_params = {})
       params = {
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
       }
 
       get :pipelines, params.merge(extra_params)
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 54b8d1108a56e773a329d3efd6d188520e11c5b5..e26731fb6916f88b817878d0faa1ef62af6ad5f9 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::CommitsController do
       context "when the ref does not exist with the suffix" do
         it "renders as atom" do
           get(:show,
-              namespace_id: project.namespace.to_param,
-              project_id: project.to_param,
+              namespace_id: project.namespace,
+              project_id: project,
               id: "master.atom")
 
           expect(response).to be_success
@@ -33,8 +33,8 @@ describe Projects::CommitsController do
           allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
 
           get(:show,
-              namespace_id: project.namespace.to_param,
-              project_id: project.to_param,
+              namespace_id: project.namespace,
+              project_id: project,
               id: "master.atom")
         end
 
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index e811c76fb31f21fd7cd156140f9cdb1c81df8938..15ac4e0925a965e611c674280dc2c7df08e2d071 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CompareController do
 
   it 'compare shows some diffs' do
     get(:show,
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        namespace_id: project.namespace,
+        project_id: project,
         from: ref_from,
         to: ref_to)
 
@@ -25,8 +25,8 @@ describe Projects::CompareController do
 
   it 'compare shows some diffs with ignore whitespace change option' do
     get(:show,
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        namespace_id: project.namespace,
+        project_id: project,
         from: '08f22f25',
         to: '66eceea0',
         w: 1)
@@ -43,8 +43,8 @@ describe Projects::CompareController do
   describe 'non-existent refs' do
     it 'uses invalid source ref' do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           from: 'non-existent',
           to: ref_to)
 
@@ -55,8 +55,8 @@ describe Projects::CompareController do
 
     it 'uses invalid target ref' do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           from: ref_from,
           to: 'non-existent')
 
@@ -67,8 +67,8 @@ describe Projects::CompareController do
 
     it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
       post(:create,
-           namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           namespace_id: project.namespace,
+           project_id: project,
            from: '',
            to: 'master')
 
@@ -77,8 +77,8 @@ describe Projects::CompareController do
 
     it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
       post(:create,
-           namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           namespace_id: project.namespace,
+           project_id: project,
            from: 'master',
            to: '')
 
@@ -87,8 +87,8 @@ describe Projects::CompareController do
 
     it 'redirects back to index when params[:from] and params[:to] are empty' do
       post(:create,
-           namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           namespace_id: project.namespace,
+           project_id: project,
            from: '',
            to: '')
 
@@ -99,8 +99,8 @@ describe Projects::CompareController do
   describe 'GET diff_for_path' do
     def diff_for_path(extra_params = {})
       params = {
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
       }
 
       get :diff_for_path, params.merge(extra_params)
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 6a6d71a16ee77f76b65ee7a26bcf86c88bbb01cb..6fae52edbadf018105204262a80ec9f9572d2991 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CycleAnalyticsController do
     context 'with no data' do
       it 'is true' do
         get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param)
+            namespace_id: project.namespace,
+            project_id: project)
 
         expect(response).to be_success
         expect(assigns(:cycle_analytics_no_data)).to eq(true)
@@ -32,8 +32,8 @@ describe Projects::CycleAnalyticsController do
 
       it 'is false' do
         get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param)
+            namespace_id: project.namespace,
+            project_id: project)
 
         expect(response).to be_success
         expect(assigns(:cycle_analytics_no_data)).to eq(false)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 84d119f1867d2ab76861f5208a146500f81af067..83d80b376fba4f40febb69eadfa1eabdab7a9712 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do
     end
   end
 
+  describe 'GET #metrics' do
+    before do
+      allow(controller).to receive(:environment).and_return(environment)
+    end
+
+    context 'when environment has no metrics' do
+      before do
+        expect(environment).to receive(:metrics).and_return(nil)
+      end
+
+      it 'returns a metrics page' do
+        get :metrics, environment_params
+
+        expect(response).to be_ok
+      end
+
+      context 'when requesting metrics as JSON' do
+        it 'returns a metrics JSON document' do
+          get :metrics, environment_params(format: :json)
+
+          expect(response).to have_http_status(204)
+          expect(json_response).to eq({})
+        end
+      end
+    end
+
+    context 'when environment has some metrics' do
+      before do
+        expect(environment).to receive(:metrics).and_return({
+          success: true,
+          metrics: {},
+          last_update: 42
+        })
+      end
+
+      it 'returns a metrics JSON document' do
+        get :metrics, environment_params(format: :json)
+
+        expect(response).to be_ok
+        expect(json_response['success']).to be(true)
+        expect(json_response['metrics']).to eq({})
+        expect(json_response['last_update']).to eq(42)
+      end
+    end
+  end
+
   def environment_params(opts = {})
     opts.reverse_merge(namespace_id: project.namespace,
                        project_id: project,
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index a4884256c928fa0f13b7a58c4048553dcd74812f..6a5433bcc9c75360818ff905ee703cb0731a427c 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::FindFileController do
 
     before do
       get(:show,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: id)
     end
 
@@ -36,8 +36,8 @@ describe Projects::FindFileController do
   describe "GET #list" do
     def go(format: 'json')
       get :list,
-          namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          namespace_id: project.namespace,
+          project_id: project,
           id: id,
           format: format
     end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index a867668d97b4b037feb72c81a2b39749a04e4540..8282d79298fb13fa63f7110c6ea85bf7154fdd2b 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -9,8 +9,8 @@ describe Projects::ForksController do
   describe 'GET index' do
     def get_forks
       get :index,
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
     end
 
     context 'when fork is public' do
@@ -71,8 +71,8 @@ describe Projects::ForksController do
   describe 'GET new' do
     def get_new
       get :new,
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        namespace_id: project.namespace,
+        project_id: project
     end
 
     context 'when user is signed in' do
@@ -99,8 +99,8 @@ describe Projects::ForksController do
   describe 'POST create' do
     def post_create
       post :create,
-        namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        namespace_id: project.namespace,
+        project_id: project,
         namespace_key: user.namespace.id
     end
 
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index bbe8e4bf6b243e507d9ad010df46d9de37649e82..e0de62e4454706c5859834a2984bec698d1a948c 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -9,23 +9,39 @@ describe Projects::GraphsController do
     project.team << [user, :master]
   end
 
-  describe 'GET #languages' do
+  describe 'GET languages' do
+    it "redirects_to action charts" do
+      get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+      expect(response).to redirect_to action: :charts
+    end
+  end
+
+  describe 'GET commits' do
+    it "redirects_to action charts" do
+      get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+      expect(response).to redirect_to action: :charts
+    end
+  end
+
+  describe 'GET charts' do
     let(:linguist_repository) do
       double(languages: {
                'Ruby'         => 1000,
                'CoffeeScript' => 350,
-               'PowerShell'   => 15
+               'NSIS'         => 15
              })
     end
 
     let(:expected_values) do
-      ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+      nsis_color = "##{Digest::SHA256.hexdigest('NSIS')[0...6]}"
       [
         # colors from Linguist:
-        { label: "Ruby",         color: "#701516", highlight: "#701516" },
-        { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+        { label: "Ruby",         color: "#701516",  highlight: "#701516" },
+        { label: "CoffeeScript", color: "#244776",  highlight: "#244776" },
         # colors from SHA256 fallback:
-        { label: "PowerShell",   color: ps_color,  highlight: ps_color  }
+        { label: "NSIS",         color: nsis_color, highlight: nsis_color }
       ]
     end
 
@@ -34,7 +50,7 @@ describe Projects::GraphsController do
     end
 
     it 'sets the correct colour according to language' do
-      get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+      get(:charts, namespace_id: project.namespace, project_id: project, id: 'master')
 
       expected_values.each do |val|
         expect(assigns(:languages)).to include(a_hash_including(val))
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index a976a9c27abb67f246f6ab50a978b38b51aa5048..ca4a8e871c0f6058cdc1aab4415a603ce455d272 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::GroupLinksController do
   describe '#create' do
     shared_context 'link project to group' do
       before do
-        post(:create, namespace_id: project.namespace.to_param,
-                      project_id: project.to_param,
+        post(:create, namespace_id: project.namespace,
+                      project_id: project,
                       link_group_id: group.id,
                       link_group_access: ProjectGroupLink.default_access)
       end
@@ -50,8 +50,8 @@ describe Projects::GroupLinksController do
 
     context 'when project group id equal link group id' do
       before do
-        post(:create, namespace_id: project.namespace.to_param,
-                      project_id: project.to_param,
+        post(:create, namespace_id: project.namespace,
+                      project_id: project,
                       link_group_id: group2.id,
                       link_group_access: ProjectGroupLink.default_access)
       end
@@ -69,8 +69,8 @@ describe Projects::GroupLinksController do
 
     context 'when link group id is not present' do
       before do
-        post(:create, namespace_id: project.namespace.to_param,
-                      project_id: project.to_param,
+        post(:create, namespace_id: project.namespace,
+                      project_id: project,
                       link_group_access: ProjectGroupLink.default_access)
       end
 
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 2acbba469e3e83b732bc26959423132afbc34f13..7c75815f3c460902a2ef82d0b0dd007ddeb79445 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::ImportsController do
       end
 
       it 'renders template' do
-        get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+        get :show, namespace_id: project.namespace.to_param, project_id: project
 
         expect(response).to render_template :show
       end
 
       it 'sets flash.now if params is present' do
-        get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
+        get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'Started' }
 
         expect(flash.now[:notice]).to eq 'Started'
       end
@@ -39,13 +39,13 @@ describe Projects::ImportsController do
         end
 
         it 'renders template' do
-          get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+          get :show, namespace_id: project.namespace.to_param, project_id: project
 
           expect(response).to render_template :show
         end
 
         it 'sets flash.now if params is present' do
-          get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
+          get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'In progress' }
 
           expect(flash.now[:notice]).to eq 'In progress'
         end
@@ -57,7 +57,7 @@ describe Projects::ImportsController do
         end
 
         it 'redirects to new_namespace_project_import_path' do
-          get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+          get :show, namespace_id: project.namespace.to_param, project_id: project
 
           expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project)
         end
@@ -72,7 +72,7 @@ describe Projects::ImportsController do
           it 'redirects to namespace_project_path' do
             allow_any_instance_of(Project).to receive(:forked?).and_return(true)
 
-            get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+            get :show, namespace_id: project.namespace.to_param, project_id: project
 
             expect(flash[:notice]).to eq 'The project was successfully forked.'
             expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -81,7 +81,7 @@ describe Projects::ImportsController do
 
         context 'when project is external' do
           it 'redirects to namespace_project_path' do
-            get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+            get :show, namespace_id: project.namespace.to_param, project_id: project
 
             expect(flash[:notice]).to eq 'The project was successfully imported.'
             expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -97,7 +97,7 @@ describe Projects::ImportsController do
           end
 
           it 'redirects to params[:to]' do
-            get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params
+            get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params
 
             expect(flash[:notice]).to eq params[:notice]
             expect(response).to redirect_to params[:to]
@@ -111,7 +111,7 @@ describe Projects::ImportsController do
         end
 
         it 'redirects to namespace_project_path' do
-          get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+          get :show, namespace_id: project.namespace.to_param, project_id: project
 
           expect(response).to redirect_to namespace_project_path(project.namespace, project)
         end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 7871b6a9e106ece4769a9adbd19f6b8d43578c53..57a921e3676800394489cd6b5c819d3ec862eddc 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::IssuesController do
         allow(project).to receive(:external_issue_tracker).and_return(external)
         controller.instance_variable_set(:@project, project)
 
-        get :index, namespace_id: project.namespace.path, project_id: project
+        get :index, namespace_id: project.namespace, project_id: project
 
         expect(response).to redirect_to('https://example.com/project')
       end
@@ -27,13 +27,13 @@ describe Projects::IssuesController do
       it_behaves_like "issuables list meta-data", :issue
 
       it "returns index" do
-        get :index, namespace_id: project.namespace.path, project_id: project.path
+        get :index, namespace_id: project.namespace, project_id: project
 
         expect(response).to have_http_status(200)
       end
 
       it "returns 301 if request path doesn't match project path" do
-        get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+        get :index, namespace_id: project.namespace, project_id: project.path.upcase
 
         expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
       end
@@ -42,7 +42,7 @@ describe Projects::IssuesController do
         project.issues_enabled = false
         project.save
 
-        get :index, namespace_id: project.namespace.path, project_id: project.path
+        get :index, namespace_id: project.namespace, project_id: project
         expect(response).to have_http_status(404)
       end
 
@@ -50,7 +50,7 @@ describe Projects::IssuesController do
         controller.instance_variable_set(:@project, project)
         allow(project).to receive(:default_issues_tracker?).and_return(false)
 
-        get :index, namespace_id: project.namespace.path, project_id: project.path
+        get :index, namespace_id: project.namespace, project_id: project
         expect(response).to have_http_status(404)
       end
     end
@@ -67,8 +67,8 @@ describe Projects::IssuesController do
 
       it 'redirects to last_page if page number is larger than number of pages' do
         get :index,
-          namespace_id: project.namespace.path.to_param,
-          project_id: project.path.to_param,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
           page: (last_page + 1).to_param
 
         expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
@@ -76,8 +76,8 @@ describe Projects::IssuesController do
 
       it 'redirects to specified page' do
         get :index,
-          namespace_id: project.namespace.path.to_param,
-          project_id: project.path.to_param,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
           page: last_page.to_param
 
         expect(assigns(:issues).current_page).to eq(last_page)
@@ -87,6 +87,12 @@ describe Projects::IssuesController do
   end
 
   describe 'GET #new' do
+    it 'redirects to signin if not logged in' do
+      get :new, namespace_id: project.namespace, project_id: project
+
+      expect(response).to redirect_to(new_user_session_path)
+    end
+
     context 'internal issue tracker' do
       before do
         sign_in(user)
@@ -94,7 +100,7 @@ describe Projects::IssuesController do
       end
 
       it 'builds a new issue' do
-        get :new, namespace_id: project.namespace.path, project_id: project
+        get :new, namespace_id: project.namespace, project_id: project
 
         expect(assigns(:issue)).to be_a_new(Issue)
       end
@@ -104,7 +110,16 @@ describe Projects::IssuesController do
         project_with_repository.team << [user, :developer]
         mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
 
-        get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
+        get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid
+
+        expect(assigns(:issue).title).not_to be_empty
+        expect(assigns(:issue).description).not_to be_empty
+      end
+
+      it 'fills in an issue for a discussion' do
+        note = create(:note_on_merge_request, project: project)
+
+        get :new, namespace_id: project.namespace.path, project_id: project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id
 
         expect(assigns(:issue).title).not_to be_empty
         expect(assigns(:issue).description).not_to be_empty
@@ -112,12 +127,17 @@ describe Projects::IssuesController do
     end
 
     context 'external issue tracker' do
+      before do
+        sign_in(user)
+        project.team << [user, :developer]
+      end
+
       it 'redirects to the external issue tracker' do
         external = double(new_issue_path: 'https://example.com/issues/new')
         allow(project).to receive(:external_issue_tracker).and_return(external)
         controller.instance_variable_set(:@project, project)
 
-        get :new, namespace_id: project.namespace.path, project_id: project
+        get :new, namespace_id: project.namespace, project_id: project
 
         expect(response).to redirect_to('https://example.com/issues/new')
       end
@@ -125,13 +145,33 @@ describe Projects::IssuesController do
   end
 
   describe 'PUT #update' do
-    context 'when moving issue to another private project' do
-      let(:another_project) { create(:empty_project, :private) }
+    before do
+      sign_in(user)
+      project.team << [user, :developer]
+    end
 
-      before do
-        sign_in(user)
-        project.team << [user, :developer]
+    it_behaves_like 'update invalid issuable', Issue
+
+    context 'changing the assignee' do
+      it 'limits the attributes exposed on the assignee' do
+        assignee = create(:user)
+        project.add_developer(assignee)
+
+        put :update,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
+          id: issue.iid,
+          issue: { assignee_id: assignee.id },
+          format: :json
+        body = JSON.parse(response.body)
+
+        expect(body['assignee'].keys)
+          .to match_array(%w(name username avatar_url))
       end
+    end
+
+    context 'when moving issue to another private project' do
+      let(:another_project) { create(:empty_project, :private) }
 
       context 'when user has access to move issue' do
         before { another_project.team << [user, :reporter] }
@@ -251,7 +291,7 @@ describe Projects::IssuesController do
       def update_issue(issue_params = {}, additional_params = {})
         params = {
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: issue.iid,
           issue: issue_params
         }.merge(additional_params)
@@ -262,7 +302,7 @@ describe Projects::IssuesController do
       def move_issue
         put :update,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: issue.iid,
           issue: { title: 'New title' },
           move_to_project_id: another_project.id
@@ -342,7 +382,7 @@ describe Projects::IssuesController do
       def get_issues
         get :index,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param
+          project_id: project
       end
     end
 
@@ -405,7 +445,7 @@ describe Projects::IssuesController do
       def go(id:)
         get :show,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: id
       end
     end
@@ -416,7 +456,7 @@ describe Projects::IssuesController do
       def go(id:)
         get :edit,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: id
       end
     end
@@ -427,7 +467,7 @@ describe Projects::IssuesController do
       def go(id:)
         put :update,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: id,
           issue: { title: 'New title' }
       end
@@ -442,7 +482,7 @@ describe Projects::IssuesController do
 
       post :create, {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        project_id: project,
         issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
       }.merge(additional_params)
 
@@ -460,11 +500,11 @@ describe Projects::IssuesController do
       end
 
       let(:merge_request_params) do
-        { merge_request_for_resolving_discussions: merge_request.iid }
+        { merge_request_to_resolve_discussions_of: merge_request.iid }
       end
 
-      def post_issue(issue_params)
-        post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
+      def post_issue(issue_params, other_params: {})
+        post :create, { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params)
       end
 
       it 'creates an issue for the project' do
@@ -483,6 +523,27 @@ describe Projects::IssuesController do
 
         expect(discussion.resolved?).to eq(true)
       end
+
+      it 'sets a flash message' do
+        post_issue(title: 'Hello')
+
+        expect(flash[:notice]).to eq('Resolved all discussions.')
+      end
+
+      describe "resolving a single discussion" do
+        before do
+          post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id })
+        end
+        it 'resolves a single discussion' do
+          discussion.first_note.reload
+
+          expect(discussion.resolved?).to eq(true)
+        end
+
+        it 'sets a flash message that one discussion was resolved' do
+          expect(flash[:notice]).to eq('Resolved 1 discussion.')
+        end
+      end
     end
 
     context 'Akismet is enabled' do
@@ -607,8 +668,8 @@ describe Projects::IssuesController do
         project.team << [admin, :master]
         sign_in(admin)
         post :mark_as_spam, {
-          namespace_id: project.namespace.path,
-          project_id: project.path,
+          namespace_id: project.namespace,
+          project_id: project,
           id: issue.iid
         }
       end
@@ -624,7 +685,7 @@ describe Projects::IssuesController do
     context "when the user is a developer" do
       before { sign_in(user) }
       it "rejects a developer to destroy an issue" do
-        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+        delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
         expect(response).to have_http_status(404)
       end
     end
@@ -637,7 +698,7 @@ describe Projects::IssuesController do
       before { sign_in(owner) }
 
       it "deletes the issue" do
-        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+        delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
 
         expect(response).to have_http_status(302)
         expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
@@ -646,7 +707,7 @@ describe Projects::IssuesController do
       it 'delegates the update of the todos count cache to TodoService' do
         expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
 
-        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+        delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
       end
     end
   end
@@ -659,8 +720,8 @@ describe Projects::IssuesController do
 
     it "toggles the award emoji" do
       expect do
-        post(:toggle_award_emoji, namespace_id: project.namespace.path,
-                                  project_id: project.path, id: issue.iid, name: "thumbsup")
+        post(:toggle_award_emoji, namespace_id: project.namespace,
+                                  project_id: project, id: issue.iid, name: "thumbsup")
       end.to change { issue.award_emoji.count }.by(1)
 
       expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3e0326dd47d79cfa4d12e6a633cb6d7af5f2b271..6a6e9bf378abdb25a7e22b937b065f28e47d46b1 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -67,7 +67,7 @@ describe Projects::LabelsController do
     end
 
     def list_labels
-      get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+      get :index, namespace_id: project.namespace.to_param, project_id: project
     end
   end
 
@@ -76,7 +76,7 @@ describe Projects::LabelsController do
       let(:personal_project) { create(:empty_project, namespace: user.namespace) }
 
       it 'creates labels' do
-        post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+        post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project
 
         expect(response).to have_http_status(302)
       end
@@ -84,7 +84,7 @@ describe Projects::LabelsController do
 
     context 'project belonging to a group' do
       it 'creates labels' do
-        post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+        post :generate, namespace_id: project.namespace.to_param, project_id: project
 
         expect(response).to have_http_status(302)
       end
@@ -109,7 +109,7 @@ describe Projects::LabelsController do
     end
 
     def toggle_subscription(label)
-      post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
+      post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project, id: label.to_param
     end
   end
 
@@ -119,7 +119,7 @@ describe Projects::LabelsController do
 
     context 'not group owner' do
       it 'denies access' do
-        post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+        post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
 
         expect(response).to have_http_status(404)
       end
@@ -131,13 +131,13 @@ describe Projects::LabelsController do
       end
 
       it 'gives access' do
-        post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+        post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
 
         expect(response).to redirect_to(namespace_project_labels_path)
       end
 
       it 'promotes the label' do
-        post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+        post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
 
         expect(Label.where(id: label_1.id)).to be_empty
         expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil
@@ -151,7 +151,7 @@ describe Projects::LabelsController do
         end
 
         it 'returns to label list' do
-          post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+          post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
           expect(response).to redirect_to(namespace_project_labels_path)
         end
       end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index cae733f0cfb8ea9591f4bab168ae72041c8ead6d..c5abf11cfa5df2472db7cc57990a7e514e7ca661 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::MattermostsController do
     it 'accepts the request' do
       get(:new,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param)
+          project_id: project)
 
       expect(response).to have_http_status(200)
     end
@@ -30,7 +30,7 @@ describe Projects::MattermostsController do
     subject do
       post(:create,
            namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           project_id: project,
            mattermost: mattermost_params)
     end
 
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index f84f922ba5e046571a3e92d32d7f97c895972874..c310d830e817ebefa3fc2fdf1bff3f460308731d 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -43,7 +43,8 @@ describe Projects::MergeRequestsController do
           submit_new_merge_request(format: :json)
 
           expect(response).to be_ok
-          expect(json_response).not_to be_empty
+          expect(json_response).to have_key 'pipelines'
+          expect(json_response['pipelines']).not_to be_empty
         end
       end
     end
@@ -51,10 +52,11 @@ describe Projects::MergeRequestsController do
     def submit_new_merge_request(format: :html)
       get :new,
           namespace_id: fork_project.namespace.to_param,
-          project_id: fork_project.to_param,
+          project_id: fork_project,
           merge_request: {
             source_branch: 'remove-submodule',
-            target_branch: 'master' },
+            target_branch: 'master'
+          },
           format: format
     end
   end
@@ -63,7 +65,7 @@ describe Projects::MergeRequestsController do
     it "loads labels into the @labels variable" do
       get action,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: merge_request.iid,
           format: 'html'
       expect(assigns(:labels)).not_to be_nil
@@ -75,7 +77,7 @@ describe Projects::MergeRequestsController do
       it "does generally work" do
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: format)
 
@@ -89,7 +91,7 @@ describe Projects::MergeRequestsController do
 
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: format)
       end
@@ -97,7 +99,7 @@ describe Projects::MergeRequestsController do
       it "renders it" do
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: format)
 
@@ -110,7 +112,7 @@ describe Projects::MergeRequestsController do
 
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: format)
 
@@ -125,7 +127,7 @@ describe Projects::MergeRequestsController do
       it "triggers workhorse to serve the request" do
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: :diff)
 
@@ -137,7 +139,7 @@ describe Projects::MergeRequestsController do
       it 'triggers workhorse to serve the request' do
         get(:show,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: :patch)
 
@@ -152,7 +154,7 @@ describe Projects::MergeRequestsController do
     def get_merge_requests(page = nil)
       get :index,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           state: 'opened', page: page.to_param
     end
 
@@ -201,6 +203,24 @@ describe Projects::MergeRequestsController do
   end
 
   describe 'PUT update' do
+    context 'changing the assignee' do
+      it 'limits the attributes exposed on the assignee' do
+        assignee = create(:user)
+        project.add_developer(assignee)
+
+        put :update,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
+          id: merge_request.iid,
+          merge_request: { assignee_id: assignee.id },
+          format: :json
+        body = JSON.parse(response.body)
+
+        expect(body['assignee'].keys)
+          .to match_array(%w(name username avatar_url))
+      end
+    end
+
     context 'there is no source project' do
       let(:project)       { create(:project) }
       let(:fork_project)  { create(:forked_project_with_submodules) }
@@ -215,8 +235,8 @@ describe Projects::MergeRequestsController do
 
       it 'closes MR without errors' do
         post :update,
-            namespace_id: project.namespace.path,
-            project_id: project.path,
+            namespace_id: project.namespace,
+            project_id: project,
             id: merge_request.iid,
             merge_request: {
               state_event: 'close'
@@ -230,8 +250,8 @@ describe Projects::MergeRequestsController do
         merge_request.close!
 
         put :update,
-            namespace_id: project.namespace.path,
-            project_id: project.path,
+            namespace_id: project.namespace,
+            project_id: project,
             id: merge_request.iid,
             merge_request: {
               title: 'New title'
@@ -245,8 +265,8 @@ describe Projects::MergeRequestsController do
         merge_request.close!
 
         put :update,
-            namespace_id: project.namespace.path,
-            project_id: project.path,
+            namespace_id: project.namespace,
+            project_id: project,
             id: merge_request.iid,
             merge_request: {
               target_branch: 'new_branch'
@@ -254,14 +274,16 @@ describe Projects::MergeRequestsController do
 
         expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
       end
+
+      it_behaves_like 'update invalid issuable', MergeRequest
     end
   end
 
   describe 'POST merge' do
     let(:base_params) do
       {
-        namespace_id: project.namespace.path,
-        project_id: project.path,
+        namespace_id: project.namespace,
+        project_id: project,
         id: merge_request.iid,
         format: 'raw'
       }
@@ -316,41 +338,41 @@ describe Projects::MergeRequestsController do
         merge_with_sha
       end
 
-      context 'when merge_when_build_succeeds is passed' do
-        def merge_when_build_succeeds
-          post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1')
+      context 'when the pipeline succeeds is passed' do
+        def merge_when_pipeline_succeeds
+          post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
         end
 
         before do
           create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
         end
 
-        it 'returns :merge_when_build_succeeds' do
-          merge_when_build_succeeds
+        it 'returns :merge_when_pipeline_succeeds' do
+          merge_when_pipeline_succeeds
 
-          expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+          expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
         end
 
-        it 'sets the MR to merge when the build succeeds' do
-          service = double(:merge_when_build_succeeds_service)
+        it 'sets the MR to merge when the pipeline succeeds' do
+          service = double(:merge_when_pipeline_succeeds_service)
 
           expect(MergeRequests::MergeWhenPipelineSucceedsService)
             .to receive(:new).with(project, anything, anything)
             .and_return(service)
           expect(service).to receive(:execute).with(merge_request)
 
-          merge_when_build_succeeds
+          merge_when_pipeline_succeeds
         end
 
-        context 'when project.only_allow_merge_if_build_succeeds? is true' do
+        context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
           before do
-            project.update_column(:only_allow_merge_if_build_succeeds, true)
+            project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
           end
 
-          it 'returns :merge_when_build_succeeds' do
-            merge_when_build_succeeds
+          it 'returns :merge_when_pipeline_succeeds' do
+            merge_when_pipeline_succeeds
 
-            expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+            expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
           end
         end
       end
@@ -425,7 +447,7 @@ describe Projects::MergeRequestsController do
 
   describe "DELETE destroy" do
     it "denies access to users unless they're admin or project owner" do
-      delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+      delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
 
       expect(response).to have_http_status(404)
     end
@@ -438,7 +460,7 @@ describe Projects::MergeRequestsController do
       before { sign_in owner }
 
       it "deletes the merge request" do
-        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+        delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
 
         expect(response).to have_http_status(302)
         expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
@@ -447,7 +469,7 @@ describe Projects::MergeRequestsController do
       it 'delegates the update of the todos count cache to TodoService' do
         expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
 
-        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+        delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
       end
     end
   end
@@ -456,7 +478,7 @@ describe Projects::MergeRequestsController do
     def go(extra_params = {})
       params = {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        project_id: project,
         id: merge_request.iid
       }
 
@@ -536,7 +558,7 @@ describe Projects::MergeRequestsController do
     def diff_for_path(extra_params = {})
       params = {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param
+        project_id: project
       }
 
       get :diff_for_path, params.merge(extra_params)
@@ -600,7 +622,7 @@ describe Projects::MergeRequestsController do
 
         before do
           other_project.team << [user, :master]
-          diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
+          diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project)
         end
 
         it 'returns a 404' do
@@ -666,7 +688,7 @@ describe Projects::MergeRequestsController do
     def go(format: 'html')
       get :commits,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: merge_request.iid,
           format: format
     end
@@ -706,7 +728,7 @@ describe Projects::MergeRequestsController do
       before do
         get :pipelines,
             namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
+            project_id: project,
             id: merge_request.iid,
             format: :json
       end
@@ -725,7 +747,7 @@ describe Projects::MergeRequestsController do
 
         get :conflicts,
             namespace_id: merge_request_with_conflicts.project.namespace.to_param,
-            project_id: merge_request_with_conflicts.project.to_param,
+            project_id: merge_request_with_conflicts.project,
             id: merge_request_with_conflicts.iid,
             format: 'json'
       end
@@ -743,7 +765,7 @@ describe Projects::MergeRequestsController do
       before do
         get :conflicts,
             namespace_id: merge_request_with_conflicts.project.namespace.to_param,
-            project_id: merge_request_with_conflicts.project.to_param,
+            project_id: merge_request_with_conflicts.project,
             id: merge_request_with_conflicts.iid,
             format: 'json'
       end
@@ -772,7 +794,7 @@ describe Projects::MergeRequestsController do
 
             section['lines'].each do |line|
               if section['conflict']
-                expect(line['type']).to be_in(['old', 'new'])
+                expect(line['type']).to be_in(%w(old new))
                 expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
               else
                 if line['type'].nil?
@@ -806,7 +828,7 @@ describe Projects::MergeRequestsController do
 
       post :remove_wip,
            namespace_id: merge_request.project.namespace.to_param,
-           project_id: merge_request.project.to_param,
+           project_id: merge_request.project,
            id: merge_request.iid
 
       expect(merge_request.reload.title).to eq(merge_request.wipless_title)
@@ -817,7 +839,7 @@ describe Projects::MergeRequestsController do
     def conflict_for_path(path)
       get :conflict_for_path,
           namespace_id: merge_request_with_conflicts.project.namespace.to_param,
-          project_id: merge_request_with_conflicts.project.to_param,
+          project_id: merge_request_with_conflicts.project,
           id: merge_request_with_conflicts.iid,
           old_path: path,
           new_path: path,
@@ -873,7 +895,7 @@ describe Projects::MergeRequestsController do
     def resolve_conflicts(files)
       post :resolve_conflicts,
            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
-           project_id: merge_request_with_conflicts.project.to_param,
+           project_id: merge_request_with_conflicts.project,
            id: merge_request_with_conflicts.iid,
            format: 'json',
            files: files,
@@ -1024,7 +1046,7 @@ describe Projects::MergeRequestsController do
 
       post :assign_related_issues,
            namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           project_id: project,
            id: merge_request.iid
     end
 
@@ -1079,7 +1101,7 @@ describe Projects::MergeRequestsController do
 
         get :ci_environments_status,
           namespace_id: merge_request.project.namespace.to_param,
-          project_id: merge_request.project.to_param,
+          project_id: merge_request.project,
           id: merge_request.iid, format: 'json'
       end
 
@@ -1092,8 +1114,8 @@ describe Projects::MergeRequestsController do
   describe 'GET merge_widget_refresh' do
     let(:params) do
       {
-        namespace_id: project.namespace.path,
-        project_id: project.path,
+        namespace_id: project.namespace,
+        project_id: project,
         id: merge_request.iid,
         format: :raw
       }
@@ -1131,14 +1153,14 @@ describe Projects::MergeRequestsController do
     end
 
     context 'when waiting for build' do
-      let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) }
+      let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) }
 
       it 'returns an OK response' do
         expect(response).to have_http_status(:ok)
       end
 
-      it 'sets status to :merge_when_build_succeeds' do
-        expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+      it 'sets status to :merge_when_pipeline_succeeds' do
+        expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
         expect(response).to render_template('merge')
       end
     end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index dc597202050fe6d157bca87c97ca1f390232bd18..d80780b1d90eb00252d708980760352208f74f7f 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -200,4 +200,31 @@ describe Projects::NotesController do
       end
     end
   end
+
+  describe 'GET index' do
+    let(:last_fetched_at) { '1487756246' }
+    let(:request_params) do
+      {
+        namespace_id: project.namespace,
+        project_id: project,
+        target_type: 'issue',
+        target_id: issue.id
+      }
+    end
+
+    before do
+      sign_in(user)
+      project.team << [user, :developer]
+    end
+
+    it 'passes last_fetched_at from headers to NotesFinder' do
+      request.headers['X-Last-Fetched-At'] = last_fetched_at
+
+      expect(NotesFinder).to receive(:new)
+        .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
+        .and_call_original
+
+      get :index, request_params
+    end
+  end
 end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1ed2ee3ab4afd9f309dd9fc80488f3edb74ca3bf..04bb5cbbd591a59e13e54b2cdff3aa3762e11d35 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -12,10 +12,13 @@ describe Projects::PipelinesController do
 
   describe 'GET index.json' do
     before do
-      create_list(:ci_empty_pipeline, 2, project: project)
+      create(:ci_empty_pipeline, status: 'pending', project: project)
+      create(:ci_empty_pipeline, status: 'running', project: project)
+      create(:ci_empty_pipeline, status: 'created', project: project)
+      create(:ci_empty_pipeline, status: 'success', project: project)
 
-      get :index, namespace_id: project.namespace.path,
-                  project_id: project.path,
+      get :index, namespace_id: project.namespace,
+                  project_id: project,
                   format: :json
     end
 
@@ -23,9 +26,11 @@ describe Projects::PipelinesController do
       expect(response).to have_http_status(:ok)
 
       expect(json_response).to include('pipelines')
-      expect(json_response['pipelines'].count).to eq 2
-      expect(json_response['count']['all']).to eq 2
-      expect(json_response['count']['running_or_pending']).to eq 2
+      expect(json_response['pipelines'].count).to eq 4
+      expect(json_response['count']['all']).to eq 4
+      expect(json_response['count']['running']).to eq 1
+      expect(json_response['count']['pending']).to eq 1
+      expect(json_response['count']['finished']).to eq 1
     end
   end
 
@@ -57,8 +62,8 @@ describe Projects::PipelinesController do
     end
 
     def get_stage(name)
-      get :stage, namespace_id: project.namespace.path,
-                  project_id: project.path,
+      get :stage, namespace_id: project.namespace,
+                  project_id: project,
                   id: pipeline.id,
                   stage: name,
                   format: :json
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index da6112a13f7587ab365ef38b770dd1bfbd5ff12c..e378b5714fef7ec953aaa40b43f73997397e2e28 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::ProtectedBranchesController do
   describe "GET #index" do
     let(:project) { create(:project_empty_repo, :public) }
     it "redirects empty repo to projects page" do
-      get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
+      get(:index, namespace_id: project.namespace.to_param, project_id: project)
     end
   end
 end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b23d6e257babee8d13f052d1aa0a7e54967af242..952071af57ff432556f8150ab6193e799e255028 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -3,21 +3,21 @@ require 'spec_helper'
 describe Projects::RawController do
   let(:public_project) { create(:project, :public, :repository) }
 
-  describe "#show" do
+  describe '#show' do
     context 'regular filename' do
       let(:id) { 'master/README.md' }
 
       it 'delivers ASCII file' do
         get(:show,
             namespace_id: public_project.namespace.to_param,
-            project_id: public_project.to_param,
+            project_id: public_project,
             id: id)
 
         expect(response).to have_http_status(200)
         expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
         expect(response.header['Content-Disposition']).
-            to eq("inline")
-        expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+            to eq('inline')
+        expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
       end
     end
 
@@ -27,12 +27,12 @@ describe Projects::RawController do
       it 'sets image content type header' do
         get(:show,
             namespace_id: public_project.namespace.to_param,
-            project_id: public_project.to_param,
+            project_id: public_project,
             id: id)
 
         expect(response).to have_http_status(200)
         expect(response.header['Content-Type']).to eq('image/jpeg')
-        expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+        expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
       end
     end
 
@@ -40,32 +40,57 @@ describe Projects::RawController do
       let(:id) { 'be93687/files/lfs/lfs_object.iso' }
       let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
 
-      context 'when project has access' do
+      context 'when lfs is enabled' do
         before do
-          public_project.lfs_objects << lfs_object
-          allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
-          allow(controller).to receive(:send_file) { controller.head :ok }
+          allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
         end
 
-        it 'serves the file' do
-          expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment')
-          get(:show,
-              namespace_id: public_project.namespace.to_param,
-              project_id: public_project.to_param,
-              id: id)
+        context 'when project has access' do
+          before do
+            public_project.lfs_objects << lfs_object
+            allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+            allow(controller).to receive(:send_file) { controller.head :ok }
+          end
 
-          expect(response).to have_http_status(200)
+          it 'serves the file' do
+            expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
+            get(:show,
+                namespace_id: public_project.namespace.to_param,
+                project_id: public_project,
+                id: id)
+
+            expect(response).to have_http_status(200)
+          end
+        end
+
+        context 'when project does not have access' do
+          it 'does not serve the file' do
+            get(:show,
+                namespace_id: public_project.namespace.to_param,
+                project_id: public_project,
+                id: id)
+
+            expect(response).to have_http_status(404)
+          end
         end
       end
 
-      context 'when project does not have access' do
-        it 'does not serve the file' do
+      context 'when lfs is not enabled' do
+        before do
+          allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+        end
+
+        it 'delivers ASCII file' do
           get(:show,
               namespace_id: public_project.namespace.to_param,
-              project_id: public_project.to_param,
+              project_id: public_project,
               id: id)
 
-          expect(response).to have_http_status(404)
+          expect(response).to have_http_status(200)
+          expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+          expect(response.header['Content-Disposition']).
+              to eq('inline')
+          expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
         end
       end
     end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index d8fb4667c674ebe2b88191da60f404fa701d51bc..3a3e7467ef2500f1a9552231abc9f7b53483d8b3 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::RefsController do
     def default_get(format = :html)
       get :logs_tree,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: 'master',
           path: 'foo/bar/baz.html',
           format: format
@@ -23,7 +23,7 @@ describe Projects::RefsController do
       xhr :get,
           :logs_tree,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param, id: 'master',
+          project_id: project, id: 'master',
           path: 'foo/bar/baz.html', format: format
     end
 
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 69fcc26c77e4729232f261e1e66601098d9d6692..358f26dfb02b212b0c216ac33a77a021fc7b893f 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ReleasesController do
       tag_id = release.tag
       project.releases.destroy_all
 
-      get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+      get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id
 
       release = assigns(:release)
       expect(release).not_to be_nil
@@ -24,7 +24,7 @@ describe Projects::ReleasesController do
     end
 
     it 'retrieves an existing release' do
-      get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+      get :edit, namespace_id: project.namespace, project_id: project, tag_id: release.tag
 
       release = assigns(:release)
       expect(release).not_to be_nil
@@ -48,7 +48,7 @@ describe Projects::ReleasesController do
   def update_release(description)
     put :update,
       namespace_id: project.namespace.to_param,
-      project_id: project.to_param,
+      project_id: project,
       tag_id: release.tag,
       release: { description: description }
   end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 04e88879fb8403d686918b57773daeaf8e2fa09f..9c55d159fa03357b5a66ac6f306aa42ebc4be952 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::RepositoriesController do
   describe "GET archive" do
     context 'as a guest' do
       it 'responds with redirect in correct format' do
-        get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
+        get :archive, namespace_id: project.namespace, project_id: project, format: "zip"
 
         expect(response.header["Content-Type"]).to start_with('text/html')
         expect(response).to be_redirect
@@ -22,7 +22,7 @@ describe Projects::RepositoriesController do
       end
 
       it "uses Gitlab::Workhorse" do
-        get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+        get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
 
         expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
       end
@@ -33,7 +33,7 @@ describe Projects::RepositoriesController do
         end
 
         it "renders Not Found" do
-          get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+          get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
 
           expect(response).to have_http_status(404)
         end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f73471f8ca8d0154d0d6e46a3e8dd8dbaf745293
--- /dev/null
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::Settings::RepositoryController do
+  let(:project) { create(:project_empty_repo, :public) }
+  let(:user) { create(:user) }
+
+  before do
+    project.add_master(user)
+    sign_in(user)
+  end
+
+  describe 'GET show' do
+    it 'renders show with 200 status code' do
+      get :show, namespace_id: project.namespace, project_id: project
+
+      expect(response).to have_http_status(200)
+      expect(response).to render_template(:show)
+    end
+  end
+end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 8bab094a79efe8c82fe91ac15401eb2c8675c654..24a59caff4e7e22a95ad8f46fa88ed3f52acd9d2 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -17,16 +17,16 @@ describe Projects::SnippetsController do
 
       it 'redirects to last_page if page number is larger than number of pages' do
         get :index,
-          namespace_id: project.namespace.path,
-          project_id: project.path, page: (last_page + 1).to_param
+          namespace_id: project.namespace,
+          project_id: project, page: (last_page + 1).to_param
 
         expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
       end
 
       it 'redirects to specified page' do
         get :index,
-          namespace_id: project.namespace.path,
-          project_id: project.path, page: last_page.to_param
+          namespace_id: project.namespace,
+          project_id: project, page: last_page.to_param
 
         expect(assigns(:snippets).current_page).to eq(last_page)
         expect(response).to have_http_status(200)
@@ -38,7 +38,7 @@ describe Projects::SnippetsController do
 
       context 'when anonymous' do
         it 'does not include the private snippet' do
-          get :index, namespace_id: project.namespace.path, project_id: project.path
+          get :index, namespace_id: project.namespace, project_id: project
 
           expect(assigns(:snippets)).not_to include(project_snippet)
           expect(response).to have_http_status(200)
@@ -49,7 +49,7 @@ describe Projects::SnippetsController do
         before { sign_in(user) }
 
         it 'renders the snippet' do
-          get :index, namespace_id: project.namespace.path, project_id: project.path
+          get :index, namespace_id: project.namespace, project_id: project
 
           expect(assigns(:snippets)).to include(project_snippet)
           expect(response).to have_http_status(200)
@@ -60,7 +60,7 @@ describe Projects::SnippetsController do
         before { sign_in(user2) }
 
         it 'renders the snippet' do
-          get :index, namespace_id: project.namespace.path, project_id: project.path
+          get :index, namespace_id: project.namespace, project_id: project
 
           expect(assigns(:snippets)).to include(project_snippet)
           expect(response).to have_http_status(200)
@@ -77,7 +77,7 @@ describe Projects::SnippetsController do
 
       post :create, {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        project_id: project,
         project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
       }.merge(additional_params)
     end
@@ -152,7 +152,7 @@ describe Projects::SnippetsController do
 
       put :update, {
         namespace_id: project.namespace.to_param,
-        project_id: project.to_param,
+        project_id: project,
         id: snippet.id,
         project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
       }.merge(additional_params)
@@ -281,8 +281,8 @@ describe Projects::SnippetsController do
       sign_in(admin)
 
       post :mark_as_spam,
-           namespace_id: project.namespace.path,
-           project_id: project.path,
+           namespace_id: project.namespace,
+           project_id: project,
            id: snippet.id
     end
 
@@ -300,7 +300,7 @@ describe Projects::SnippetsController do
 
         context 'when anonymous' do
           it 'responds with status 404' do
-            get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+            get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
 
             expect(response).to have_http_status(404)
           end
@@ -310,7 +310,7 @@ describe Projects::SnippetsController do
           before { sign_in(user) }
 
           it 'renders the snippet' do
-            get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+            get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
 
             expect(assigns(:snippet)).to eq(project_snippet)
             expect(response).to have_http_status(200)
@@ -321,7 +321,7 @@ describe Projects::SnippetsController do
           before { sign_in(user2) }
 
           it 'renders the snippet' do
-            get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+            get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
 
             expect(assigns(:snippet)).to eq(project_snippet)
             expect(response).to have_http_status(200)
@@ -332,7 +332,7 @@ describe Projects::SnippetsController do
       context 'when the project snippet does not exist' do
         context 'when anonymous' do
           it 'responds with status 404' do
-            get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+            get action, namespace_id: project.namespace, project_id: project, id: 42
 
             expect(response).to have_http_status(404)
           end
@@ -342,7 +342,7 @@ describe Projects::SnippetsController do
           before { sign_in(user) }
 
           it 'responds with status 404' do
-            get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+            get action, namespace_id: project.namespace, project_id: project, id: 42
 
             expect(response).to have_http_status(404)
           end
@@ -364,8 +364,8 @@ describe Projects::SnippetsController do
     context 'CRLF line ending' do
       let(:params) do
         {
-          namespace_id: project.namespace.path,
-          project_id: project.path,
+          namespace_id: project.namespace,
+          project_id: project,
           id: project_snippet.to_param
         }
       end
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index c36a5fdd66cab0835c36241f26dc12801bd3443f..fc97bac64cd131ea0ee5833f0dbcb444ef911ddd 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::TagsController do
   let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
 
   describe 'GET index' do
-    before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param }
+    before { get :index, namespace_id: project.namespace.to_param, project_id: project }
 
     it 'returns the tags for the page' do
       expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
@@ -19,7 +19,7 @@ describe Projects::TagsController do
   end
 
   describe 'GET show' do
-    before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+    before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id }
 
     context "valid tag" do
       let(:id) { 'v1.0.0' }
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 80f84a388ceef2a55bc0c5a6d1ea6baee2bb9f81..70e7f9ca96e88f5848106d2fe5f09d719db88d9f 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -14,13 +14,13 @@ describe Projects::TemplatesController do
 
   before do
     project.add_user(user, Gitlab::Access::MASTER)
-    project.repository.commit_file(user, file_path_1, 'something valid',
-      message: 'test 3', branch_name: 'master', update: false)
+    project.repository.create_file(user, file_path_1, 'something valid',
+      message: 'test 3', branch_name: 'master')
   end
 
   describe '#show' do
     it 'renders template name and content as json' do
-      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
 
       expect(response.status).to eq(200)
       expect(body["name"]).to eq("bug")
@@ -29,21 +29,21 @@ describe Projects::TemplatesController do
 
     it 'renders 404 when unauthorized' do
       sign_in(user2)
-      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+      get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
 
       expect(response.status).to eq(404)
     end
 
     it 'renders 404 when template type is not found' do
       sign_in(user)
-      get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+      get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json)
 
       expect(response.status).to eq(404)
     end
 
     it 'renders 404 without errors' do
       sign_in(user)
-      expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+      expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json) }.not_to raise_error
     end
   end
 end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 415c264e0dd1852192d0988573c0ddd781922872..9a7beeff6fe22f8920adab8816f25fe8cecff715 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -12,8 +12,8 @@ describe Projects::TodosController do
     describe 'POST create' do
       def go
         post :create,
-          namespace_id: project.namespace.path,
-          project_id: project.path,
+          namespace_id: project.namespace,
+          project_id: project,
           issuable_id: issue.id,
           issuable_type: 'issue',
           format: 'html'
@@ -80,8 +80,8 @@ describe Projects::TodosController do
     describe 'POST create' do
       def go
         post :create,
-          namespace_id: project.namespace.path,
-          project_id: project.path,
+          namespace_id: project.namespace,
+          project_id: project,
           issuable_id: merge_request.id,
           issuable_type: 'merge_request',
           format: 'html'
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index b81645a3d2d069e114122a2f7dd8e1dd83fd2696..ab94e292e481a5460b5fe4484b267f8d90511f10 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::TreeController do
     before do
       get(:show,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: id)
     end
 
@@ -74,7 +74,7 @@ describe Projects::TreeController do
     before do
       get(:show,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           id: id)
     end
 
@@ -94,7 +94,7 @@ describe Projects::TreeController do
     before do
       post(:create_dir,
            namespace_id: project.namespace.to_param,
-           project_id: project.to_param,
+           project_id: project,
            id: 'master',
            dir_name: path,
            target_branch: target_branch,
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 0347e789576ec81ce940912ecdfa73895bf5dbc4..cd6961a7bd52cc860369c3cd7dfcb20d2f22cd11 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::UploadsController do
       it "returns an error" do
         post :create,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           format: :json
         expect(response).to have_http_status(422)
       end
@@ -26,7 +26,7 @@ describe Projects::UploadsController do
       before do
         post :create,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           file: jpg,
           format: :json
       end
@@ -35,13 +35,26 @@ describe Projects::UploadsController do
         expect(response.body).to match '\"alt\":\"rails_sample\"'
         expect(response.body).to match "\"url\":\"/uploads"
       end
+
+      # NOTE: This is as close as we're getting to an Integration test for this
+      # behavior. We're avoiding a proper Feature test because those should be
+      # testing things entirely user-facing, which the Upload model is very much
+      # not.
+      it 'creates a corresponding Upload record' do
+        upload = Upload.last
+
+        aggregate_failures do
+          expect(upload).to exist
+          expect(upload.model).to eq project
+        end
+      end
     end
 
     context 'with valid non-image file' do
       before do
         post :create,
           namespace_id: project.namespace.to_param,
-          project_id: project.to_param,
+          project_id: project,
           file: txt,
           format: :json
       end
@@ -57,7 +70,7 @@ describe Projects::UploadsController do
     let(:go) do
       get :show,
         namespace_id: project.namespace.to_param,
-        project_id:   project.to_param,
+        project_id:   project,
         secret:       "123456",
         filename:     "image.jpg"
     end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 9fa358f7d6211672bddca6c0ef95cf8579aaf834..1ecfe48475cbaaa17fc989dea634d818b165dc57 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::VariablesController do
   describe 'POST #create' do
     context 'variable is valid' do
       it 'shows a success flash message' do
-        post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+        post :create, namespace_id: project.namespace.to_param, project_id: project,
                       variable: { key: "one", value: "two" }
 
         expect(flash[:notice]).to include 'Variables were successfully updated.'
@@ -22,7 +22,7 @@ describe Projects::VariablesController do
 
     context 'variable is invalid' do
       it 'shows an alert flash message' do
-        post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+        post :create, namespace_id: project.namespace.to_param, project_id: project,
                       variable: { key: "..one", value: "two" }
 
         expect(response).to render_template("projects/variables/show")
@@ -35,12 +35,12 @@ describe Projects::VariablesController do
 
     context 'updating a variable with valid characters' do
       before do
-        variable.gl_project_id = project.id
+        variable.project_id = project.id
         project.variables << variable
       end
 
       it 'shows a success flash message' do
-        post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+        post :update, namespace_id: project.namespace.to_param, project_id: project,
                       id: variable.id, variable: { key: variable.key, value: 'two' }
 
         expect(flash[:notice]).to include 'Variable was successfully updated.'
@@ -48,7 +48,7 @@ describe Projects::VariablesController do
       end
 
       it 'renders the action #show if the variable key is invalid' do
-        post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+        post :update, namespace_id: project.namespace.to_param, project_id: project,
                       id: variable.id, variable: { key: '?', value: variable.value }
 
         expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e7aa8745b996a12b8e37a96777ff4dbdb7fc88fc..a88ffc1ea6a3cbdad3f739592f3d09e9ce94aab8 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -35,7 +35,7 @@ describe ProjectsController do
         let(:private_project) { create(:empty_project, :private) }
 
         it "does not initialize notification setting" do
-          get :show, namespace_id: private_project.namespace.path, id: private_project.path
+          get :show, namespace_id: private_project.namespace, id: private_project
           expect(assigns(:notification_setting)).to be_nil
         end
       end
@@ -43,7 +43,7 @@ describe ProjectsController do
       context "user has access to project" do
         context "and does not have notification setting" do
           it "initializes notification as disabled" do
-            get :show, namespace_id: public_project.namespace.path, id: public_project.path
+            get :show, namespace_id: public_project.namespace, id: public_project
             expect(assigns(:notification_setting).level).to eq("global")
           end
         end
@@ -56,7 +56,7 @@ describe ProjectsController do
           end
 
           it "shows current notification setting" do
-            get :show, namespace_id: public_project.namespace.path, id: public_project.path
+            get :show, namespace_id: public_project.namespace, id: public_project
             expect(assigns(:notification_setting).level).to eq("watch")
           end
         end
@@ -71,24 +71,26 @@ describe ProjectsController do
         end
 
         it 'shows wiki homepage' do
-          get :show, namespace_id: project.namespace.path, id: project.path
+          get :show, namespace_id: project.namespace, id: project
 
           expect(response).to render_template('projects/_wiki')
         end
 
         it 'shows issues list page if wiki is disabled' do
           project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+          create(:issue, project: project)
 
-          get :show, namespace_id: project.namespace.path, id: project.path
+          get :show, namespace_id: project.namespace, id: project
 
           expect(response).to render_template('projects/issues/_issues')
+          expect(assigns(:issuable_meta_data)).not_to be_nil
         end
 
         it 'shows customize workflow page if wiki and issues are disabled' do
           project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
           project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
 
-          get :show, namespace_id: project.namespace.path, id: project.path
+          get :show, namespace_id: project.namespace, id: project
 
           expect(response).to render_template("projects/_customize_workflow")
         end
@@ -96,7 +98,7 @@ describe ProjectsController do
         it 'shows activity if enabled by user' do
           user.update_attribute(:project_view, 'activity')
 
-          get :show, namespace_id: project.namespace.path, id: project.path
+          get :show, namespace_id: project.namespace, id: project
 
           expect(response).to render_template("projects/_activity")
         end
@@ -113,7 +115,7 @@ describe ProjectsController do
           before do
             user.update_attributes(project_view: project_view)
 
-            get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+            get :show, namespace_id: empty_project.namespace, id: empty_project
           end
 
           it "renders the empty project view" do
@@ -133,7 +135,7 @@ describe ProjectsController do
           before do
             user.update_attributes(project_view: project_view)
 
-            get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+            get :show, namespace_id: empty_project.namespace, id: empty_project
           end
 
           it "renders the empty project view" do
@@ -154,23 +156,15 @@ describe ProjectsController do
         allow(controller).to receive(:current_user).and_return(user)
         allow(user).to receive(:project_view).and_return('activity')
 
-        get :show, namespace_id: public_project.namespace.path, id: public_project.path
+        get :show, namespace_id: public_project.namespace, id: public_project
         expect(response).to render_template('_activity')
       end
 
-      it "renders the readme view" do
-        allow(controller).to receive(:current_user).and_return(user)
-        allow(user).to receive(:project_view).and_return('readme')
-
-        get :show, namespace_id: public_project.namespace.path, id: public_project.path
-        expect(response).to render_template('_readme')
-      end
-
       it "renders the files view" do
         allow(controller).to receive(:current_user).and_return(user)
         allow(user).to receive(:project_view).and_return('files')
 
-        get :show, namespace_id: public_project.namespace.path, id: public_project.path
+        get :show, namespace_id: public_project.namespace, id: public_project
         expect(response).to render_template('_files')
       end
     end
@@ -178,7 +172,7 @@ describe ProjectsController do
     context "when requested with case sensitive namespace and project path" do
       context "when there is a match with the same casing" do
         it "loads the project" do
-          get :show, namespace_id: public_project.namespace.path, id: public_project.path
+          get :show, namespace_id: public_project.namespace, id: public_project
 
           expect(assigns(:project)).to eq(public_project)
           expect(response).to have_http_status(200)
@@ -187,10 +181,10 @@ describe ProjectsController do
 
       context "when there is a match with different casing" do
         it "redirects to the normalized path" do
-          get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+          get :show, namespace_id: public_project.namespace, id: public_project.path.upcase
 
           expect(assigns(:project)).to eq(public_project)
-          expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+          expect(response).to redirect_to("/#{public_project.full_path}")
         end
       end
     end
@@ -208,7 +202,7 @@ describe ProjectsController do
         project = create(:empty_project, pending_delete: true)
         sign_in(user)
 
-        get :show, namespace_id: project.namespace.path, id: project.path
+        get :show, namespace_id: project.namespace, id: project
 
         expect(response.status).to eq 404
       end
@@ -218,7 +212,7 @@ describe ProjectsController do
       it 'redirects to project page (format.html)' do
         project = create(:project, :public)
 
-        get :show, namespace_id: project.namespace.path, id: project.path, format: :git
+        get :show, namespace_id: project.namespace, id: project, format: :git
 
         expect(response).to have_http_status(302)
         expect(response).to redirect_to(namespace_project_path)
@@ -239,7 +233,7 @@ describe ProjectsController do
       sign_in(admin)
 
       put :update,
-          namespace_id: project.namespace.to_param,
+          namespace_id: project.namespace,
           id: project.id,
           project: project_params
 
@@ -257,7 +251,7 @@ describe ProjectsController do
       sign_in(admin)
 
       orig_id = project.id
-      delete :destroy, namespace_id: project.namespace.path, id: project.path
+      delete :destroy, namespace_id: project.namespace, id: project
 
       expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
       expect(response).to have_http_status(302)
@@ -277,7 +271,7 @@ describe ProjectsController do
         project.merge_requests << merge_request
         sign_in(admin)
 
-        delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path
+        delete :destroy, namespace_id: fork_project.namespace, id: fork_project
 
         expect(merge_request.reload.state).to eq('closed')
       end
@@ -287,8 +281,8 @@ describe ProjectsController do
   describe 'PUT #new_issue_address' do
     subject do
       put :new_issue_address,
-        namespace_id: project.namespace.to_param,
-        id: project.to_param
+        namespace_id: project.namespace,
+        id: project
       user.reload
     end
 
@@ -316,23 +310,23 @@ describe ProjectsController do
       sign_in(user)
       expect(user.starred?(public_project)).to be_falsey
       post(:toggle_star,
-           namespace_id: public_project.namespace.to_param,
-           id: public_project.to_param)
+           namespace_id: public_project.namespace,
+           id: public_project)
       expect(user.starred?(public_project)).to be_truthy
       post(:toggle_star,
-           namespace_id: public_project.namespace.to_param,
-           id: public_project.to_param)
+           namespace_id: public_project.namespace,
+           id: public_project)
       expect(user.starred?(public_project)).to be_falsey
     end
 
     it "does nothing if user is not signed in" do
       post(:toggle_star,
-           namespace_id: project.namespace.to_param,
-           id: public_project.to_param)
+           namespace_id: project.namespace,
+           id: public_project)
       expect(user.starred?(public_project)).to be_falsey
       post(:toggle_star,
-           namespace_id: project.namespace.to_param,
-           id: public_project.to_param)
+           namespace_id: project.namespace,
+           id: public_project)
       expect(user.starred?(public_project)).to be_falsey
     end
   end
@@ -366,8 +360,8 @@ describe ProjectsController do
 
         it 'does nothing if project was not forked' do
           delete(:remove_fork,
-              namespace_id: unforked_project.namespace.to_param,
-              id: unforked_project.to_param, format: :js)
+              namespace_id: unforked_project.namespace,
+              id: unforked_project, format: :js)
 
           expect(flash[:notice]).to be_nil
           expect(response).to render_template(:remove_fork)
@@ -377,8 +371,8 @@ describe ProjectsController do
 
     it "does nothing if user is not signed in" do
       delete(:remove_fork,
-          namespace_id: project.namespace.to_param,
-          id: project.to_param, format: :js)
+          namespace_id: project.namespace,
+          id: project, format: :js)
       expect(response).to have_http_status(401)
     end
   end
@@ -387,7 +381,7 @@ describe ProjectsController do
     let(:public_project) { create(:project, :public) }
 
     it "gets a list of branches and tags" do
-      get :refs, namespace_id: public_project.namespace.path, id: public_project.path
+      get :refs, namespace_id: public_project.namespace, id: public_project
 
       parsed_body = JSON.parse(response.body)
       expect(parsed_body["Branches"]).to include("master")
@@ -396,7 +390,7 @@ describe ProjectsController do
     end
 
     it "gets a list of branches, tags and commits" do
-      get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+      get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456"
 
       parsed_body = JSON.parse(response.body)
       expect(parsed_body["Branches"]).to include("master")
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index b14d275f7faecff9f9f52da8c5e25eb35b155eed..b32eb39b1fb278efe1df8f5280e9e540efec57c3 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -2,6 +2,26 @@ require 'spec_helper'
 
 describe RootController do
   describe 'GET index' do
+    context 'when user is not logged in' do
+      it 'redirects to the sign-in page' do
+        get :index
+
+        expect(response).to redirect_to(new_user_session_path)
+      end
+
+      context 'when a custom home page URL is defined' do
+        before do
+          stub_application_setting(home_page_url: 'https://gitlab.com')
+        end
+
+        it 'redirects the user to the custom home page URL' do
+          get :index
+
+          expect(response).to redirect_to('https://gitlab.com')
+        end
+      end
+    end
+
     context 'with a user' do
       let(:user) { create(:user) }
 
@@ -12,55 +32,60 @@ describe RootController do
 
       context 'who has customized their dashboard setting for starred projects' do
         before do
-          user.update_attribute(:dashboard, 'stars')
+          user.dashboard = 'stars'
         end
 
         it 'redirects to their specified dashboard' do
           get :index
+
           expect(response).to redirect_to starred_dashboard_projects_path
         end
       end
 
       context 'who has customized their dashboard setting for project activities' do
         before do
-          user.update_attribute(:dashboard, 'project_activity')
+          user.dashboard = 'project_activity'
         end
 
         it 'redirects to the activity list' do
           get :index
+
           expect(response).to redirect_to activity_dashboard_path
         end
       end
 
       context 'who has customized their dashboard setting for starred project activities' do
         before do
-          user.update_attribute(:dashboard, 'starred_project_activity')
+          user.dashboard = 'starred_project_activity'
         end
 
         it 'redirects to the activity list' do
           get :index
+
           expect(response).to redirect_to activity_dashboard_path(filter: 'starred')
         end
       end
 
       context 'who has customized their dashboard setting for groups' do
         before do
-          user.update_attribute(:dashboard, 'groups')
+          user.dashboard = 'groups'
         end
 
         it 'redirects to their group list' do
           get :index
+
           expect(response).to redirect_to dashboard_groups_path
         end
       end
 
       context 'who has customized their dashboard setting for todos' do
         before do
-          user.update_attribute(:dashboard, 'todos')
+          user.dashboard = 'todos'
         end
 
         it 'redirects to their todo list' do
           get :index
+
           expect(response).to redirect_to dashboard_todos_path
         end
       end
@@ -68,6 +93,7 @@ describe RootController do
       context 'who uses the default dashboard setting' do
         it 'renders the default dashboard' do
           get :index
+
           expect(response).to render_template 'dashboard/projects/index'
         end
       end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index b56c7880b64291076c4109075be75fb8085471aa..a06c29dd91a9dc3dbeee3d98d81dc6366739ed1f 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -25,9 +25,17 @@ describe SessionsController do
           expect(subject.current_user). to eq user
         end
 
-        it "creates an audit log record" do
+        it 'creates an audit log record' do
           expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
-          expect(SecurityEvent.last.details[:with]).to eq("standard")
+          expect(SecurityEvent.last.details[:with]).to eq('standard')
+        end
+
+        include_examples 'user login request with unique ip limit', 302 do
+          def request
+            post(:create, user: { login: user.username, password: user.password })
+            expect(subject.current_user).to eq user
+            subject.sign_out user
+          end
         end
       end
     end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index c9584ddf18c6e4b24a9a209d2a7307c07656e513..f67d26da0ac980673d4de8388a275cafad7d71f9 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,4 +1,9 @@
 require 'spec_helper'
+shared_examples 'content not cached without revalidation' do
+  it 'ensures content will not be cached without revalidation' do
+    expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate')
+  end
+end
 
 describe UploadsController do
   let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
@@ -50,6 +55,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+              response
+            end
+          end
         end
       end
 
@@ -59,6 +71,13 @@ describe UploadsController do
 
           expect(response).to have_http_status(200)
         end
+
+        it_behaves_like 'content not cached without revalidation' do
+          subject do
+            get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+            response
+          end
+        end
       end
     end
 
@@ -76,6 +95,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+              response
+            end
+          end
         end
 
         context "when signed in" do
@@ -88,6 +114,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+              response
+            end
+          end
         end
       end
 
@@ -133,6 +166,13 @@ describe UploadsController do
 
                 expect(response).to have_http_status(200)
               end
+
+              it_behaves_like 'content not cached without revalidation' do
+                subject do
+                  get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+                  response
+                end
+              end
             end
           end
 
@@ -157,6 +197,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+              response
+            end
+          end
         end
 
         context "when signed in" do
@@ -169,6 +216,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+              response
+            end
+          end
         end
       end
 
@@ -205,6 +259,13 @@ describe UploadsController do
 
                 expect(response).to have_http_status(200)
               end
+
+              it_behaves_like 'content not cached without revalidation' do
+                subject do
+                  get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+                  response
+                end
+              end
             end
           end
 
@@ -234,6 +295,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+              response
+            end
+          end
         end
 
         context "when signed in" do
@@ -246,6 +314,13 @@ describe UploadsController do
 
             expect(response).to have_http_status(200)
           end
+
+          it_behaves_like 'content not cached without revalidation' do
+            subject do
+              get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+              response
+            end
+          end
         end
       end
 
@@ -291,6 +366,13 @@ describe UploadsController do
 
                 expect(response).to have_http_status(200)
               end
+
+              it_behaves_like 'content not cached without revalidation' do
+                subject do
+                  get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+                  response
+                end
+              end
             end
           end
 
diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb
new file mode 100644
index 0000000000000000000000000000000000000000..82f44fa3d1576441ea7ac5d4caca36e80f349444
--- /dev/null
+++ b/spec/factories/chat_teams.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+  factory :chat_team, class: ChatTeam do
+    sequence :team_id do |n|
+      "abcdefghijklm#{n}"
+    end
+
+    namespace factory: :group
+  end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index a90534d10ba2bfb6e294d4cfe24f42d1e1cf9a36..6b0d084614b31acbf56f3f8113bbbb394a1c5608 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -15,8 +15,8 @@ FactoryGirl.define do
 
     options do
       {
-        image: "ruby:2.1",
-        services: ["postgres"]
+        image: 'ruby:2.1',
+        services: ['postgres']
       }
     end
 
@@ -57,7 +57,7 @@ FactoryGirl.define do
     end
 
     trait :manual do
-      status 'skipped'
+      status 'manual'
       self.when 'manual'
     end
 
@@ -71,11 +71,26 @@ FactoryGirl.define do
       allow_failure true
     end
 
+    trait :ignored do
+      allowed_to_fail
+    end
+
     trait :playable do
-      skipped
       manual
     end
 
+    trait :tags do
+      tag_list [:docker, :ruby]
+    end
+
+    trait :on_tag do
+      tag true
+    end
+
+    trait :triggered do
+      trigger_request factory: :ci_trigger_request_with_variables
+    end
+
     after(:build) do |build, evaluator|
       build.project = build.pipeline.project
     end
@@ -151,5 +166,31 @@ FactoryGirl.define do
         allow(build).to receive(:commit).and_return build(:commit)
       end
     end
+
+    trait :extended_options do
+      options do
+        {
+            image: 'ruby:2.1',
+            services: ['postgres'],
+            after_script: "ls\ndate",
+            artifacts: {
+                name: 'artifacts_file',
+                untracked: false,
+                paths: ['out/'],
+                when: 'always',
+                expire_in: '7d'
+            },
+            cache: {
+                key: 'cache_key',
+                untracked: false,
+                paths: ['vendor/*']
+            }
+        }
+      end
+    end
+
+    trait :no_options do
+      options { {} }
+    end
   end
 end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 77404f46c92fe2ec1e460aacf887ee661d505b1e..b67c96bc00d0bf5fccbd12e29720c738663471f1 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -40,6 +40,14 @@ FactoryGirl.define do
       trait :invalid do
         config(rspec: nil)
       end
+
+      trait :blocked do
+        status :manual
+      end
+
+      trait :success do
+        status :success
+      end
     end
   end
 end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3372e5ab685708553720cc96252d9b1b4d232b05..6712dd5d82ef805261e9966526ee9f0b3d882643 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
 FactoryGirl.define do
   factory :ci_runner_project, class: Ci::RunnerProject do
     runner_id 1
-    gl_project_id 1
+    project_id 1
   end
 end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 756b341ecbad33af139ae0cd5a5789d353de7907..169590deb8ead521b07e6d157a623dfb38e5905d 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -35,6 +35,10 @@ FactoryGirl.define do
       status 'created'
     end
 
+    trait :manual do
+      status 'manual'
+    end
+
     after(:build) do |build, evaluator|
       build.project = build.pipeline.project
     end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 22f84150bb39f15636f52df8ec45047b0596e538..21487541507fcc17f789763e9e222281019d3d0b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
     author
     association :source_project, :repository, factory: :project
     target_project { source_project }
+    project { target_project }
 
     # $ git log --pretty=oneline feature..master
     # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
@@ -59,8 +60,8 @@ FactoryGirl.define do
       target_branch "master"
     end
 
-    trait :merge_when_build_succeeds do
-      merge_when_build_succeeds true
+    trait :merge_when_pipeline_succeeds do
+      merge_when_pipeline_succeeds true
       merge_user author
     end
 
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 5c50cd7f4ad77754632f10ad6dae15d006d97319..fe19a404e16da73138c4e37b6677aaab43028f6b 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -26,12 +26,17 @@ FactoryGirl.define do
 
     factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
       association :project, :repository
+
+      transient do
+        line_number 14
+      end
+
       position do
         Gitlab::Diff::Position.new(
           old_path: "files/ruby/popen.rb",
           new_path: "files/ruby/popen.rb",
           old_line: nil,
-          new_line: 14,
+          new_line: line_number,
           diff_refs: noteable.diff_refs
         )
       end
diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb
new file mode 100644
index 0000000000000000000000000000000000000000..543b3e992742577e12bbd914397e383a53e9e35b
--- /dev/null
+++ b/spec/factories/oauth_access_grants.rb
@@ -0,0 +1,11 @@
+FactoryGirl.define do
+  factory :oauth_access_grant do
+    resource_owner_id { create(:user).id }
+    application
+    token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+    expires_in 2.hours
+
+    redirect_uri { application.redirect_uri }
+    scopes { application.scopes }
+  end
+end
diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb
index ccf02d0719b1f4ae43d34f420c26901d6f4647cf..a46bc1d8ce89992fa0d59c4300d5493afa63115c 100644
--- a/spec/factories/oauth_access_tokens.rb
+++ b/spec/factories/oauth_access_tokens.rb
@@ -2,6 +2,7 @@ FactoryGirl.define do
   factory :oauth_access_token do
     resource_owner
     application
-    token '123456'
+    token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+    scopes { application.scopes }
   end
 end
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index d116a57383077095ae3af27dc3fd96c52fa30c1e..86cdc20826864e0e677faf43fc3530cae0e35116 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -1,7 +1,7 @@
 FactoryGirl.define do
   factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
     name { FFaker::Name.name }
-    uid { FFaker::Name.name }
+    uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
     redirect_uri { FFaker::Internet.uri('http') }
     owner
     owner_type 'User'
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 811eab7e15bdc298660ec61554b0e667ddb53ddb..7b15ba47de180f85b6cb0625c4abaf5930e04c3a 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -6,5 +6,22 @@ FactoryGirl.define do
     revoked false
     expires_at { 5.days.from_now }
     scopes ['api']
+    impersonation false
+
+    trait :impersonation do
+      impersonation true
+    end
+
+    trait :revoked do
+      revoked true
+    end
+
+    trait :expired do
+      expires_at { 1.day.ago }
+    end
+
+    trait :invalid do
+      token nil
+    end
   end
 end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index c80b09e9b9d574b0430eff1903c0458c5691945e..0db2fe04eddd77ee1dc70fe8c92e44bf8834e3a4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -38,13 +38,17 @@ FactoryGirl.define do
 
     trait :empty_repo do
       after(:create) do |project|
-        project.create_repository
+        raise "Failed to create repository!" unless project.create_repository
+
+        # We delete hooks so that gitlab-shell will not try to authenticate with
+        # an API that isn't running
+        FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'hooks'))
       end
     end
 
     trait :broken_repo do
       after(:create) do |project|
-        project.create_repository
+        raise "Failed to create repository!" unless project.create_repository
 
         FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
       end
@@ -138,27 +142,24 @@ FactoryGirl.define do
 
         project.add_user(args[:user], args[:access])
 
-        project.repository.commit_file(
+        project.repository.create_file(
           args[:user],
           ".gitlab/#{args[:path]}/bug.md",
           'something valid',
           message: 'test 3',
-          branch_name: 'master',
-          update: false)
-        project.repository.commit_file(
+          branch_name: 'master')
+        project.repository.create_file(
           args[:user],
           ".gitlab/#{args[:path]}/template_test.md",
           'template_test',
           message: 'test 1',
-          branch_name: 'master',
-          update: false)
-        project.repository.commit_file(
+          branch_name: 'master')
+        project.repository.create_file(
           args[:user],
           ".gitlab/#{args[:path]}/feature_proposal.md",
           'feature_proposal',
           message: 'test 2',
-          branch_name: 'master',
-          update: false)
+          branch_name: 'master')
       end
     end
   end
@@ -188,27 +189,19 @@ FactoryGirl.define do
 
   factory :jira_project, parent: :project do
     has_external_issue_tracker true
-
-    after :create do |project|
-      project.create_jira_service(
-        active: true,
-        properties: {
-          title: 'JIRA tracker',
-          url: 'http://jira.example.net',
-          project_key: 'JIRA'
-        }
-      )
-    end
+    jira_service
   end
 
   factory :kubernetes_project, parent: :empty_project do
+    kubernetes_service
+  end
+
+  factory :prometheus_project, parent: :empty_project do
     after :create do |project|
-      project.create_kubernetes_service(
+      project.create_prometheus_service(
         active: true,
         properties: {
-          namespace: project.path,
-          api_url: 'https://kubernetes.example.com',
-          token: 'a' * 40,
+          api_url: 'https://prometheus.example.com'
         }
       )
     end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index a14a46c803e623b0c6e786d1897fa8d6760563c8..88f6c2655053117ac8bd274ee78ae23c7cde6cf2 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -2,4 +2,23 @@ FactoryGirl.define do
   factory :service do
     project factory: :empty_project
   end
+
+  factory :kubernetes_service do
+    project factory: :empty_project
+    active true
+    properties({
+      namespace: 'somepath',
+      api_url: 'https://kubernetes.example.com',
+      token: 'a' * 40,
+    })
+  end
+
+  factory :jira_service do
+    project factory: :empty_project
+    active true
+    properties(
+      url: 'https://jira.example.com',
+      project_key: 'jira-key'
+    )
+  end
 end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index a5265f1b1897787ef2c2efa0d12d000dfe489cab..c1ac3bb84adee5a68248f01116ca0c3678ebe9c9 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -18,11 +18,6 @@ FactoryGirl.define do
       action { Todo::DIRECTLY_ADDRESSED }
     end
 
-    trait :on_commit do
-      commit_id RepoHelpers.sample_commit.id
-      target_type "Commit"
-    end
-
     trait :build_failed do
       action { Todo::BUILD_FAILED }
       target factory: :merge_request
@@ -48,4 +43,13 @@ FactoryGirl.define do
       state :done
     end
   end
+
+  factory :on_commit_todo, class: Todo do
+    project factory: :empty_project
+    author
+    user
+    action { Todo::ASSIGNED }
+    commit_id RepoHelpers.sample_commit.id
+    target_type "Commit"
+  end
 end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 1732b1a00817e385c19c9a5bcd1f683fc99cc085..249dabbaae8443acfb328e3304897ecf627a26be 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -26,6 +26,11 @@ FactoryGirl.define do
       two_factor_via_otp
     end
 
+    trait :ghost do
+      ghost true
+      after(:build) { |user, _| user.block! }
+    end
+
     trait :two_factor_via_otp do
       before(:create) do |user|
         user.otp_required_for_login = true
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index e8e080ce3e26ba52fc3c18fd170249e4320d89ae..273cacd82cd2e1957f1a98e44808e07aaff8aad0 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
     scenario 'shows only HTTP url' do
       visit_project
 
-      expect(page).to have_content("git clone #{project.http_url_to_repo}")
+      expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}")
       expect(page).not_to have_selector('#clone-dropdown')
     end
   end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index de42ab81face88065cb77639461bfd66e8847987..03daab12c8f2f1e659b851963012c7efce011f94 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do
     visit admin_application_settings_path
   end
 
+  scenario 'Change visibility settings' do
+    choose "application_setting_default_project_visibility_20"
+    click_button 'Save'
+
+    expect(page).to have_content "Application settings saved successfully"
+  end
+
   scenario 'Change application settings' do
     uncheck 'Gravatar enabled'
     fill_in 'Home page URL', with: 'https://about.gitlab.com/'
@@ -26,7 +33,7 @@ feature 'Admin updates settings', feature: true do
     fill_in 'Webhook', with: 'http://localhost'
     fill_in 'Username', with: 'test_user'
     fill_in 'service_push_channel', with: '#test_channel'
-    page.check('Notify only broken builds')
+    page.check('Notify only broken pipelines')
 
     check_all_events
     click_on 'Save'
@@ -50,7 +57,6 @@ feature 'Admin updates settings', feature: true do
     page.check('Note')
     page.check('Issue')
     page.check('Merge request')
-    page.check('Build')
     page.check('Pipeline')
   end
 end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ff5c2f9d407659e55d9a1793dc765c0a3833341
--- /dev/null
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do
+  let(:admin) { create(:admin) }
+  let!(:user) { create(:user) }
+
+  def active_impersonation_tokens
+    find(".table.active-tokens")
+  end
+
+  def inactive_impersonation_tokens
+    find(".table.inactive-tokens")
+  end
+
+  before { login_as(admin) }
+
+  describe "token creation" do
+    it "allows creation of a token" do
+      name = FFaker::Product.brand
+
+      visit admin_user_impersonation_tokens_path(user_id: user.username)
+      fill_in "Name", with: name
+
+      # Set date to 1st of next month
+      find_field("Expires at").trigger('focus')
+      find(".pika-next").click
+      click_on "1"
+
+      # Scopes
+      check "api"
+      check "read_user"
+
+      expect { click_on "Create Impersonation Token" }.to change { PersonalAccessTokensFinder.new(impersonation: true).execute.count }
+      expect(active_impersonation_tokens).to have_text(name)
+      expect(active_impersonation_tokens).to have_text('In')
+      expect(active_impersonation_tokens).to have_text('api')
+      expect(active_impersonation_tokens).to have_text('read_user')
+    end
+  end
+
+  describe 'active tokens' do
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+    let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+    it 'only shows impersonation tokens' do
+      visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+      expect(active_impersonation_tokens).to have_text(impersonation_token.name)
+      expect(active_impersonation_tokens).not_to have_text(personal_access_token.name)
+    end
+  end
+
+  describe "inactive tokens" do
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+    it "allows revocation of an active impersonation token" do
+      visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+      click_on "Revoke"
+
+      expect(inactive_impersonation_tokens).to have_text(impersonation_token.name)
+    end
+
+    it "moves expired tokens to the 'inactive' section" do
+      impersonation_token.update(expires_at: 5.days.ago)
+
+      visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+      expect(inactive_impersonation_tokens).to have_text(impersonation_token.name)
+    end
+  end
+end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 21ee6cedbae213fa9b69029d9ec547da9a977218..58b14e09740fab59db25c3ab518625f997e1cbe9 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
 
 describe "Dashboard Issues Feed", feature: true  do
   describe "GET /issues" do
-    let!(:user)     { create(:user) }
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
     let!(:project1) { create(:project) }
     let!(:project2) { create(:project) }
 
@@ -23,7 +24,7 @@ describe "Dashboard Issues Feed", feature: true  do
         visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
 
         link = find('link[type="application/atom+xml"]')
-        params = CGI::parse(URI.parse(link[:href]).query)
+        params = CGI.parse(URI.parse(link[:href]).query)
 
         expect(params).to include('private_token' => [user.private_token])
         expect(params).to include('state' => ['opened'])
@@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true  do
       end
 
       context "issue with basic fields" do
-        let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+        let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
 
         it "renders issue fields" do
           visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true  do
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
 
           expect(entry).to be_present
-          expect(entry).to have_selector('author email', text: issue2.author_email)
-          expect(entry).to have_selector('assignee email', text: issue2.author_email)
+          expect(entry).to have_selector('author email', text: issue2.author_public_email)
+          expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
           expect(entry).not_to have_selector('labels')
           expect(entry).not_to have_selector('milestone')
           expect(entry).to have_selector('description', text: issue2.description)
@@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true  do
       context "issue with label and milestone" do
         let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
         let!(:label1)     { create(:label, project: project1, title: 'label1') }
-        let!(:issue1)     { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+        let!(:issue1)     { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
 
         before do
           issue1.labels << label1
@@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true  do
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
 
           expect(entry).to be_present
-          expect(entry).to have_selector('author email', text: issue1.author_email)
-          expect(entry).to have_selector('assignee email', text: issue1.author_email)
+          expect(entry).to have_selector('author email', text: issue1.author_public_email)
+          expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
           expect(entry).to have_selector('labels label', text: label1.title)
           expect(entry).to have_selector('milestone', text: milestone1.title)
           expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 863412d18eb7dc36ffec875de38b4638cc47c08a..b3903ec2faf92705f45f12126c13ea1c0779f992 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
 
 describe 'Issues Feed', feature: true  do
   describe 'GET /issues' do
-    let!(:user)     { create(:user) }
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
     let!(:group)    { create(:group) }
     let!(:project)  { create(:project) }
-    let!(:issue)    { create(:issue, author: user, project: project) }
+    let!(:issue)    { create(:issue, author: user, assignee: assignee, project: project) }
 
     before do
       project.team << [user, :developer]
@@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true  do
         expect(response_headers['Content-Type']).
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
-        expect(body).to have_selector('author email', text: issue.author_email)
+        expect(body).to have_selector('author email', text: issue.author_public_email)
+        expect(body).to have_selector('assignee email', text: issue.author_public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
@@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true  do
         expect(response_headers['Content-Type']).
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
-        expect(body).to have_selector('author email', text: issue.author_email)
+        expect(body).to have_selector('author email', text: issue.author_public_email)
+        expect(body).to have_selector('assignee email', text: issue.author_public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
@@ -43,7 +46,7 @@ describe 'Issues Feed', feature: true  do
                                           :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
 
       link = find('link[type="application/atom+xml"]')
-      params = CGI::parse(URI.parse(link[:href]).query)
+      params = CGI.parse(URI.parse(link[:href]).query)
 
       expect(params).to include('private_token' => [user.private_token])
       expect(params).to include('state' => ['opened'])
@@ -54,7 +57,7 @@ describe 'Issues Feed', feature: true  do
       visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
 
       link = find('link[type="application/atom+xml"]')
-      params = CGI::parse(URI.parse(link[:href]).query)
+      params = CGI.parse(URI.parse(link[:href]).query)
 
       expect(params).to include('private_token' => [user.private_token])
       expect(params).to include('state' => ['opened'])
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index f8c3ccb416b022a802d7438e728233f6877f96a1..55e10a1a89bbe7d47e060fe36d7b80a7e8e6e6d8 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -57,11 +57,11 @@ describe "User Feed", feature: true  do
       end
 
       it 'has XHTML summaries in notes' do
-        expect(body).to match /Bug confirmed <img[^>]*\/>/
+        expect(body).to match /Bug confirmed <gl-emoji[^>]*>/
       end
 
       it 'has XHTML summaries in merge request descriptions' do
-        expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
+        expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
       end
     end
   end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 2875fc1e533dad988f9ae08e83cea81d6d7c9108..d17a418b8c341af9c7eb8f14169a09a8318a9b7a 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -49,6 +49,12 @@ describe 'Issue Boards add issue modal', :feature, :js do
 
       expect(page).not_to have_selector('.add-issues-modal')
     end
+
+    it 'does not show tooltip on add issues button' do
+      button = page.find('.filter-dropdown-container button', text: 'Add issues')
+
+      expect(button[:title]).not_to eq("Please add a list to your board first")
+    end
   end
 
   context 'issues list' do
@@ -101,6 +107,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
       it 'returns issues' do
         page.within('.add-issues-modal') do
           find('.form-control').native.send_keys(issue.title)
+          find('.form-control').native.send_keys(:enter)
+
+          wait_for_vue_resource
 
           expect(page).to have_selector('.card', count: 1)
         end
@@ -109,6 +118,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
       it 'returns no issues' do
         page.within('.add-issues-modal') do
           find('.form-control').native.send_keys('testing search')
+          find('.form-control').native.send_keys(:enter)
+
+          wait_for_vue_resource
 
           expect(page).not_to have_selector('.card')
           expect(page).not_to have_content("You haven't added any issues to your project yet")
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 1b25b51cfb2e0b4ad409dbb8bf952017765d84fb..f7e8b78b54d04e6c2ec3547c148208a64c06997a 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -28,10 +28,10 @@ describe 'Issue Boards', feature: true, js: true do
       expect(page).to have_content('Welcome to your Issue Board!')
     end
 
-    it 'disables add issues button by default' do
-      button = page.find('.issue-boards-search button', text: 'Add issues')
+    it 'shows tooltip on add issues button' do
+      button = page.find('.filter-dropdown-container button', text: 'Add issues')
 
-      expect(button[:disabled]).to eq true
+      expect(button[:"data-original-title"]).to eq("Please add a list to your board first")
     end
 
     it 'hides the blank state when clicking nevermind button' do
@@ -71,16 +71,16 @@ describe 'Issue Boards', feature: true, js: true do
     let!(:list1) { create(:list, board: board, label: planning, position: 0) }
     let!(:list2) { create(:list, board: board, label: development, position: 1) }
 
-    let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
-    let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
-    let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
-    let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
-    let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
-    let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
-    let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
-    let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+    let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
+    let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
+    let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
+    let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
+    let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
+    let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
+    let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
+    let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
     let!(:issue8) { create(:closed_issue, project: project) }
-    let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
+    let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
 
     before do
       visit namespace_project_board_path(project.namespace, project, board)
@@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do
     end
 
     it 'search done list' do
-      page.within('#js-boards-search') do
-        find('.form-control').set(issue8.title)
-      end
+      find('.filtered-search').set(issue8.title)
+      find('.filtered-search').native.send_keys(:enter)
 
       wait_for_vue_resource
 
@@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do
     end
 
     it 'search list' do
-      page.within('#js-boards-search') do
-        find('.form-control').set(issue5.title)
-      end
+      find('.filtered-search').set(issue5.title)
+      find('.filtered-search').native.send_keys(:enter)
 
       wait_for_vue_resource
 
@@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do
 
           wait_for_vue_resource
 
-          expect(find('.issue-boards-search')).to have_selector('.open')
+          expect(page).to have_css('#js-add-list.open')
         end
 
         it 'creates new list from a new label' do
@@ -359,17 +357,9 @@ describe 'Issue Boards', feature: true, js: true do
 
     context 'filtering' do
       it 'filters by author' do
-        page.within '.issues-filters' do
-          click_button('Author')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-author' do
-            click_link(user2.name)
-          end
-          wait_for_vue_resource
-
-          expect(find('.js-author-search')).to have_content(user2.name)
-        end
+        set_filter("author", user2.username)
+        click_filter_link(user2.username)
+        submit_filter
 
         wait_for_vue_resource
         wait_for_board_cards(1, 1)
@@ -377,17 +367,9 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'filters by assignee' do
-        page.within '.issues-filters' do
-          click_button('Assignee')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-assignee' do
-            click_link(user.name)
-          end
-          wait_for_vue_resource
-
-          expect(find('.js-assignee-search')).to have_content(user.name)
-        end
+        set_filter("assignee", user.username)
+        click_filter_link(user.username)
+        submit_filter
 
         wait_for_vue_resource
 
@@ -396,17 +378,9 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'filters by milestone' do
-        page.within '.issues-filters' do
-          click_button('Milestone')
-          wait_for_ajax
-
-          page.within '.milestone-filter' do
-            click_link(milestone.title)
-          end
-          wait_for_vue_resource
-
-          expect(find('.js-milestone-select')).to have_content(milestone.title)
-        end
+        set_filter("milestone", "\"#{milestone.title}\"")
+        click_filter_link(milestone.title)
+        submit_filter
 
         wait_for_vue_resource
         wait_for_board_cards(1, 1)
@@ -415,16 +389,9 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'filters by label' do
-        page.within '.issues-filters' do
-          click_button('Label')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-labels' do
-            click_link(testing.title)
-            wait_for_vue_resource
-            find('.dropdown-menu-close').click
-          end
-        end
+        set_filter("label", testing.title)
+        click_filter_link(testing.title)
+        submit_filter
 
         wait_for_vue_resource
         wait_for_board_cards(1, 1)
@@ -432,19 +399,14 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'filters by label with space after reload' do
-        page.within '.issues-filters' do
-          click_button('Label')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-labels' do
-            click_link(accepting.title)
-            wait_for_vue_resource(spinner: false)
-            find('.dropdown-menu-close').click
-          end
-        end
+        set_filter("label", "\"#{accepting.title}\"")
+        click_filter_link(accepting.title)
+        submit_filter
 
         # Test after reload
         page.evaluate_script 'window.location.reload()'
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..3))
 
         wait_for_vue_resource
 
@@ -460,26 +422,16 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'removes filtered labels' do
-        wait_for_vue_resource
+        set_filter("label", testing.title)
+        click_filter_link(testing.title)
+        submit_filter
 
-        page.within '.labels-filter' do
-          click_button('Label')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-labels' do
-            click_link(testing.title)
-            wait_for_vue_resource(spinner: false)
-          end
-
-          expect(page).to have_css('input[name="label_name[]"]', visible: false)
+        wait_for_board_cards(1, 1)
 
-          page.within '.dropdown-menu-labels' do
-            click_link(testing.title)
-            wait_for_vue_resource(spinner: false)
-          end
+        find('.clear-search').click
+        submit_filter
 
-          expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
-        end
+        wait_for_board_cards(1, 8)
       end
 
       it 'infinite scrolls list with label filter' do
@@ -487,16 +439,9 @@ describe 'Issue Boards', feature: true, js: true do
           create(:labeled_issue, project: project, labels: [planning, testing])
         end
 
-        page.within '.issues-filters' do
-          click_button('Label')
-          wait_for_ajax
-
-          page.within '.dropdown-menu-labels' do
-            click_link(testing.title)
-            wait_for_vue_resource
-            find('.dropdown-menu-close').click
-          end
-        end
+        set_filter("label", testing.title)
+        click_filter_link(testing.title)
+        submit_filter
 
         wait_for_vue_resource
 
@@ -518,18 +463,13 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'filters by multiple labels' do
-        page.within '.issues-filters' do
-          click_button('Label')
-          wait_for_ajax
+        set_filter("label", testing.title)
+        click_filter_link(testing.title)
 
-          page.within(find('.dropdown-menu-labels')) do
-            click_link(testing.title)
-            wait_for_vue_resource
-            click_link(bug.title)
-            wait_for_vue_resource
-            find('.dropdown-menu-close').click
-          end
-        end
+        set_filter("label", bug.title)
+        click_filter_link(bug.title)
+
+        submit_filter
 
         wait_for_vue_resource
 
@@ -545,14 +485,14 @@ describe 'Issue Boards', feature: true, js: true do
           wait_for_vue_resource
         end
 
+        page.within('.tokens-container') do
+          expect(page).to have_content(bug.title)
+        end
+
         wait_for_vue_resource
 
         wait_for_board_cards(1, 1)
         wait_for_empty_boards((2..3))
-
-        page.within('.labels-filter') do
-          expect(find('.dropdown-toggle-text')).to have_content(bug.title)
-        end
       end
 
       it 'removes label filter by clicking label button on issue' do
@@ -560,16 +500,13 @@ describe 'Issue Boards', feature: true, js: true do
           page.within(find('.card', match: :first)) do
             click_button(bug.title)
           end
+
           wait_for_vue_resource
 
           expect(page).to have_selector('.card', count: 1)
         end
 
         wait_for_vue_resource
-
-        page.within('.labels-filter') do
-          expect(find('.dropdown-toggle-text')).to have_content(bug.title)
-        end
       end
     end
   end
@@ -643,4 +580,20 @@ describe 'Issue Boards', feature: true, js: true do
       wait_for_board_cards(board, 0)
     end
   end
+
+  def set_filter(type, text)
+    find('.filtered-search').native.send_keys("#{type}:#{text}")
+  end
+
+  def submit_filter
+    find('.filtered-search').native.send_keys(:enter)
+  end
+
+  def click_filter_link(link_text)
+    page.within('.filtered-search-input-container') do
+      expect(page).to have_button(link_text)
+
+      click_button(link_text)
+    end
+  end
 end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c50155a6d148856d58c2cd27655259b73eaafebb
--- /dev/null
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -0,0 +1,166 @@
+require 'rails_helper'
+
+describe 'Issue Boards', :feature, :js do
+  include WaitForVueResource
+  include DragTo
+
+  let(:project) { create(:empty_project, :public) }
+  let(:board) { create(:board, project: project) }
+  let(:user) { create(:user) }
+  let(:label) { create(:label, project: project) }
+  let!(:list1) { create(:list, board: board, label: label, position: 0) }
+  let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3) }
+  let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2) }
+  let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
+
+  before do
+    project.team << [user, :master]
+
+    login_as(user)
+  end
+
+  context 'un-ordered issues' do
+    let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
+
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 2)
+    end
+
+    it 'has un-ordered issue as last issue' do
+      page.within(first('.board')) do
+        expect(all('.card').last).to have_content(issue4.title)
+      end
+    end
+
+    it 'moves un-ordered issue to top of list' do
+      drag(from_index: 3, to_index: 0)
+
+      page.within(first('.board')) do
+        expect(first('.card')).to have_content(issue4.title)
+      end
+    end
+  end
+
+  context 'ordering in list' do
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 2)
+    end
+
+    it 'moves from middle to top' do
+      drag(from_index: 1, to_index: 0)
+
+      wait_for_vue_resource
+
+      expect(first('.card')).to have_content(issue2.title)
+    end
+
+    it 'moves from middle to bottom' do
+      drag(from_index: 1, to_index: 2)
+
+      wait_for_vue_resource
+
+      expect(all('.card').last).to have_content(issue2.title)
+    end
+
+    it 'moves from top to bottom' do
+      drag(from_index: 0, to_index: 2)
+
+      wait_for_vue_resource
+
+      expect(all('.card').last).to have_content(issue3.title)
+    end
+
+    it 'moves from bottom to top' do
+      drag(from_index: 2, to_index: 0)
+
+      wait_for_vue_resource
+
+      expect(first('.card')).to have_content(issue1.title)
+    end
+
+    it 'moves from top to middle' do
+      drag(from_index: 0, to_index: 1)
+
+      wait_for_vue_resource
+
+      expect(first('.card')).to have_content(issue2.title)
+    end
+
+    it 'moves from bottom to middle' do
+      drag(from_index: 2, to_index: 1)
+
+      wait_for_vue_resource
+
+      expect(all('.card').last).to have_content(issue2.title)
+    end
+  end
+
+  context 'ordering when changing list' do
+    let(:label2) { create(:label, project: project) }
+    let!(:list2) { create(:list, board: board, label: label2, position: 1) }
+    let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) }
+    let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) }
+    let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) }
+
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 3)
+    end
+
+    it 'moves to top of another list' do
+      drag(list_from_index: 0, list_to_index: 1)
+
+      wait_for_vue_resource
+
+      expect(first('.board')).to have_selector('.card', count: 2)
+      expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+      page.within(all('.board')[1]) do
+        expect(first('.card')).to have_content(issue3.title)
+      end
+    end
+
+    it 'moves to bottom of another list' do
+      drag(list_from_index: 0, list_to_index: 1, to_index: 2)
+
+      wait_for_vue_resource
+
+      expect(first('.board')).to have_selector('.card', count: 2)
+      expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+      page.within(all('.board')[1]) do
+        expect(all('.card').last).to have_content(issue3.title)
+      end
+    end
+
+    it 'moves to index of another list' do
+      drag(list_from_index: 0, list_to_index: 1, to_index: 1)
+
+      wait_for_vue_resource
+
+      expect(first('.board')).to have_selector('.card', count: 2)
+      expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+      page.within(all('.board')[1]) do
+        expect(all('.card')[1]).to have_content(issue3.title)
+      end
+    end
+  end
+
+  def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+    drag_to(selector: selector,
+            scrollable: '#board-app',
+            list_from_index: list_from_index,
+            from_index: from_index,
+            to_index: to_index,
+            list_to_index: list_to_index)
+  end
+end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 1cf0d11d4480a73a9cacf56cfccc6d93fccefb18..e2281a7da551eba5182a7b487820ccbb88b49b82 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -1,7 +1,6 @@
 require 'rails_helper'
 
 describe 'Issue Boards add issue modal filtering', :feature, :js do
-  include WaitForAjax
   include WaitForVueResource
 
   let(:project) { create(:empty_project, :public) }
@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
 
     page.within('.add-issues-modal') do
       find('.form-control').native.send_keys('testing empty state')
+      find('.form-control').native.send_keys(:enter)
 
       wait_for_vue_resource
 
@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
   it 'restores filters when closing' do
     visit_board
 
-    page.within('.add-issues-modal') do
-      click_button 'Milestone'
-
-      wait_for_ajax
-
-      click_link 'Upcoming'
+    set_filter('milestone')
+    click_filter_link('Upcoming')
+    submit_filter
 
+    page.within('.add-issues-modal') do
       wait_for_vue_resource
 
       expect(page).to have_selector('.card', count: 0)
@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
     end
   end
 
-  context 'author' do
-    let!(:issue) { create(:issue, project: project, author: user2) }
-
-    before do
-      project.team << [user2, :developer]
+  it 'resotres filters after clicking clear button' do
+    visit_board
 
-      visit_board
-    end
+    set_filter('milestone')
+    click_filter_link('Upcoming')
+    submit_filter
 
-    it 'filters by any author' do
-      page.within('.add-issues-modal') do
-        click_button 'Author'
+    page.within('.add-issues-modal') do
+      wait_for_vue_resource
 
-        wait_for_ajax
+      expect(page).to have_selector('.card', count: 0)
 
-        click_link 'Any Author'
+      find('.clear-search').click
 
-        wait_for_vue_resource
+      wait_for_vue_resource
 
-        expect(page).to have_selector('.card', count: 2)
-      end
+      expect(page).to have_selector('.card', count: 1)
     end
+  end
 
-    it 'filters by selected user' do
-      page.within('.add-issues-modal') do
-        click_button 'Author'
+  context 'author' do
+    let!(:issue) { create(:issue, project: project, author: user2) }
+
+    before do
+      project.team << [user2, :developer]
 
-        wait_for_ajax
+      visit_board
+    end
 
-        click_link user2.name
+    it 'filters by selected user' do
+      set_filter('author')
+      click_filter_link(user2.name)
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: user2.username)
         expect(page).to have_selector('.card', count: 1)
       end
     end
@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
       visit_board
     end
 
-    it 'filters by any assignee' do
-      page.within('.add-issues-modal') do
-        click_button 'Assignee'
-
-        wait_for_ajax
-
-        click_link 'Any Assignee'
-
-        wait_for_vue_resource
-
-        expect(page).to have_selector('.card', count: 2)
-      end
-    end
-
     it 'filters by unassigned' do
-      page.within('.add-issues-modal') do
-        click_button 'Assignee'
-
-        wait_for_ajax
-
-        click_link 'Unassigned'
+      set_filter('assignee')
+      click_filter_link('No Assignee')
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: 'none')
         expect(page).to have_selector('.card', count: 1)
       end
     end
 
     it 'filters by selected user' do
-      page.within('.add-issues-modal') do
-        click_button 'Assignee'
-
-        wait_for_ajax
-
-        page.within '.dropdown-menu-user' do
-          click_link user2.name
-        end
+      set_filter('assignee')
+      click_filter_link(user2.name)
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: user2.username)
         expect(page).to have_selector('.card', count: 1)
       end
     end
@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
       visit_board
     end
 
-    it 'filters by any milestone' do
-      page.within('.add-issues-modal') do
-        click_button 'Milestone'
-
-        wait_for_ajax
-
-        click_link 'Any Milestone'
-
-        wait_for_vue_resource
-
-        expect(page).to have_selector('.card', count: 2)
-      end
-    end
-
     it 'filters by upcoming milestone' do
-      page.within('.add-issues-modal') do
-        click_button 'Milestone'
-
-        wait_for_ajax
-
-        click_link 'Upcoming'
+      set_filter('milestone')
+      click_filter_link('Upcoming')
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: 'upcoming')
         expect(page).to have_selector('.card', count: 0)
       end
     end
 
     it 'filters by selected milestone' do
-      page.within('.add-issues-modal') do
-        click_button 'Milestone'
-
-        wait_for_ajax
-
-        click_link milestone.name
+      set_filter('milestone')
+      click_filter_link(milestone.name)
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: milestone.name)
         expect(page).to have_selector('.card', count: 1)
       end
     end
@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
       visit_board
     end
 
-    it 'filters by any label' do
-      page.within('.add-issues-modal') do
-        click_button 'Label'
-
-        wait_for_ajax
-
-        click_link 'Any Label'
-
-        wait_for_vue_resource
-
-        expect(page).to have_selector('.card', count: 2)
-      end
-    end
-
     it 'filters by no label' do
-      page.within('.add-issues-modal') do
-        click_button 'Label'
-
-        wait_for_ajax
-
-        click_link 'No Label'
+      set_filter('label')
+      click_filter_link('No Label')
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: 'none')
         expect(page).to have_selector('.card', count: 1)
       end
     end
 
     it 'filters by label' do
-      page.within('.add-issues-modal') do
-        click_button 'Label'
-
-        wait_for_ajax
-
-        click_link label.title
+      set_filter('label')
+      click_filter_link(label.title)
+      submit_filter
 
+      page.within('.add-issues-modal') do
         wait_for_vue_resource
 
+        expect(page).to have_selector('.js-visual-token', text: label.title)
         expect(page).to have_selector('.card', count: 1)
       end
     end
@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
 
     click_button('Add issues')
   end
+
+  def set_filter(type, text = '')
+    find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}")
+  end
+
+  def submit_filter
+    find('.add-issues-modal .filtered-search').native.send_keys(:enter)
+  end
+
+  def click_filter_link(link_text)
+    page.within('.add-issues-modal .filtered-search-input-container') do
+      expect(page).to have_button(link_text)
+
+      click_button(link_text)
+    end
+  end
 end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 59e87b3f69cd4d562f4c7d003462c308cc270bfe..3332e07ec31a4a4b3a98ffc74e0efa24b079429c 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -11,8 +11,8 @@ describe 'Issue Boards', feature: true, js: true do
   let!(:bug)         { create(:label, project: project, name: 'Bug') }
   let!(:regression)  { create(:label, project: project, name: 'Regression') }
   let!(:stretch)     { create(:label, project: project, name: 'Stretch') }
-  let!(:issue1)      { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
-  let!(:issue2)      { create(:labeled_issue, project: project, labels: [development, stretch]) }
+  let!(:issue1)      { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
+  let!(:issue2)      { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
   let(:board)        { create(:board, project: project) }
   let!(:list)        { create(:list, board: board, label: development, position: 0) }
   let(:card) { first('.board').first('.card') }
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 324ede798feb5fbd6f4619d7b96af013f86e893c..0e305c5235886dccb908684115a7f25a4e5faa79 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -192,7 +192,7 @@ describe 'Commits' do
       commits = project.repository.commits(branch_name)
 
       commits.each do |commit|
-        expect(page).to have_content("committed #{commit.committed_date}")
+        expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
       end
     end
   end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index fec86128d039b0e69b40989d5bc6b8d55ef49ae7..55df7e45f79a0d0f41f939b74be78caa40df530e 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -2,433 +2,594 @@ require 'spec_helper'
 
 describe 'Copy as GFM', feature: true, js: true do
   include GitlabMarkdownHelper
+  include RepoHelpers
   include ActionView::Helpers::JavaScriptHelper
 
   before do
-    @feat = MarkdownFeature.new
+    login_as :admin
+  end
 
-    # `markdown` helper expects a `@project` variable
-    @project = @feat.project
+  describe 'Copying rendered GFM' do
+    before do
+      @feat = MarkdownFeature.new
 
-    visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
-  end
+      # `markdown` helper expects a `@project` variable
+      @project = @feat.project
 
-  # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
-  # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
-  # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
-  # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
+      visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+    end
 
-  # These are all in a single `it` for performance reasons.
-  it 'works', :aggregate_failures do
-    verify(
-      'nesting',
+    # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
+    # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM.
+    # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+    # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
 
-      '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
-    )
+    # These are all in a single `it` for performance reasons.
+    it 'works', :aggregate_failures do
+      verify(
+        'nesting',
 
-    verify(
-      'a real world example from the gitlab-ce README',
+        '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+      )
 
-      <<-GFM.strip_heredoc
-        # GitLab
+      verify(
+        'a real world example from the gitlab-ce README',
 
-        [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-        [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
-        [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-        [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+        <<-GFM.strip_heredoc
+          # GitLab
 
-        ## Canonical source
+          [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+          [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+          [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+          [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
 
-        The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+          ## Canonical source
 
-        ## Open source software to collaborate on code
+          The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
 
-        To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+          ## Open source software to collaborate on code
 
+          To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
 
-        - Manage Git repositories with fine grained access controls that keep your code secure
 
-        - Perform code reviews and enhance collaboration with merge requests
+          - Manage Git repositories with fine grained access controls that keep your code secure
 
-        - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+          - Perform code reviews and enhance collaboration with merge requests
 
-        - Each project can also have an issue tracker, issue board, and a wiki
+          - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
 
-        - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+          - Each project can also have an issue tracker, issue board, and a wiki
 
-        - Completely free and open source (MIT Expat license)
-      GFM
-    )
+          - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
 
-    verify(
-      'InlineDiffFilter',
+          - Completely free and open source (MIT Expat license)
+        GFM
+      )
 
-      '{-Deleted text-}',
-      '{+Added text+}'
-    )
+      verify(
+        'InlineDiffFilter',
 
-    verify(
-      'TaskListFilter',
+        '{-Deleted text-}',
+        '{+Added text+}'
+      )
 
-      '- [ ] Unchecked task',
-      '- [x] Checked task',
-      '1. [ ] Unchecked numbered task',
-      '1. [x] Checked numbered task'
-    )
+      verify(
+        'TaskListFilter',
 
-    verify(
-      'ReferenceFilter',
+        '- [ ] Unchecked task',
+        '- [x] Checked task',
+        '1. [ ] Unchecked numbered task',
+        '1. [x] Checked numbered task'
+      )
 
-      # issue reference
-      @feat.issue.to_reference,
-      # full issue reference
-      @feat.issue.to_reference(full: true),
-      # issue URL
-      namespace_project_issue_url(@project.namespace, @project, @feat.issue),
-      # issue URL with note anchor
-      namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
-      # issue link
-      "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
-      # issue link with note anchor
-      "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
-    )
+      verify(
+        'ReferenceFilter',
 
-    verify(
-      'AutolinkFilter',
+        # issue reference
+        @feat.issue.to_reference,
+        # full issue reference
+        @feat.issue.to_reference(full: true),
+        # issue URL
+        namespace_project_issue_url(@project.namespace, @project, @feat.issue),
+        # issue URL with note anchor
+        namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
+        # issue link
+        "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
+        # issue link with note anchor
+        "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+      )
 
-      'https://example.com'
-    )
+      verify(
+        'AutolinkFilter',
 
-    verify(
-      'TableOfContentsFilter',
+        'https://example.com'
+      )
 
-      '[[_TOC_]]'
-    )
+      verify(
+        'TableOfContentsFilter',
 
-    verify(
-      'EmojiFilter',
+        '[[_TOC_]]'
+      )
 
-      ':thumbsup:'
-    )
+      verify(
+        'EmojiFilter',
 
-    verify(
-      'ImageLinkFilter',
-
-      '![Image](https://example.com/image.png)'
-    )
+        ':thumbsup:'
+      )
 
-    verify(
-      'VideoLinkFilter',
+      verify(
+        'ImageLinkFilter',
+
+        '![Image](https://example.com/image.png)'
+      )
 
-      '![Video](https://example.com/video.mp4)'
-    )
+      verify(
+        'VideoLinkFilter',
 
-    verify(
-      'MathFilter: math as converted from GFM to HTML',
+        '![Video](https://example.com/video.mp4)'
+      )
 
-      '$`c = \pm\sqrt{a^2 + b^2}`$',
+      verify(
+        'MathFilter: math as converted from GFM to HTML',
 
-      # math block
-      <<-GFM.strip_heredoc
-        ```math
-        c = \pm\sqrt{a^2 + b^2}
-        ```
-      GFM
-    )
+        '$`c = \pm\sqrt{a^2 + b^2}`$',
 
-    aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
-      gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+        # math block
+        <<-GFM.strip_heredoc
+          ```math
+          c = \pm\sqrt{a^2 + b^2}
+          ```
+        GFM
+      )
 
-      html = <<-HTML.strip_heredoc
-        <span class="katex">
-          <span class="katex-mathml">
-            <math>
-              <semantics>
-                <mrow>
-                  <mi>c</mi>
-                  <mo>=</mo>
-                  <mo>±</mo>
-                  <msqrt>
-                    <mrow>
-                      <msup>
-                        <mi>a</mi>
-                        <mn>2</mn>
-                      </msup>
-                      <mo>+</mo>
-                      <msup>
-                        <mi>b</mi>
-                        <mn>2</mn>
-                      </msup>
-                    </mrow>
-                  </msqrt>
-                </mrow>
-                <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
-              </semantics>
-            </math>
-          </span>
-          <span class="katex-html" aria-hidden="true">
-              <span class="strut" style="height: 0.913389em;"></span>
-              <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
-              <span class="base textstyle uncramped">
-                <span class="mord mathit">c</span>
-                <span class="mrel">=</span>
-                <span class="mord">±</span>
-                <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
-                  <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
-                </span>
-                <span class="vlist">
-                  <span class="" style="top: 0em;">
-                    <span class="fontsize-ensurer reset-size5 size5">
-                      <span class="" style="font-size: 1em;">​</span>
-                    </span>
-                    <span class="mord textstyle cramped">
-                      <span class="mord">
-                        <span class="mord mathit">a</span>
-                        <span class="msupsub">
-                          <span class="vlist">
-                            <span class="" style="top: -0.289em; margin-right: 0.05em;">
-                              <span class="fontsize-ensurer reset-size5 size5">
-                                <span class="" style="font-size: 0em;">​</span>
-                              </span>
-                              <span class="reset-textstyle scriptstyle cramped">
-                                <span class="mord mathrm">2</span>
+      aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+        gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+
+        html = <<-HTML.strip_heredoc
+          <span class="katex">
+            <span class="katex-mathml">
+              <math>
+                <semantics>
+                  <mrow>
+                    <mi>c</mi>
+                    <mo>=</mo>
+                    <mo>±</mo>
+                    <msqrt>
+                      <mrow>
+                        <msup>
+                          <mi>a</mi>
+                          <mn>2</mn>
+                        </msup>
+                        <mo>+</mo>
+                        <msup>
+                          <mi>b</mi>
+                          <mn>2</mn>
+                        </msup>
+                      </mrow>
+                    </msqrt>
+                  </mrow>
+                  <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
+                </semantics>
+              </math>
+            </span>
+            <span class="katex-html" aria-hidden="true">
+                <span class="strut" style="height: 0.913389em;"></span>
+                <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
+                <span class="base textstyle uncramped">
+                  <span class="mord mathit">c</span>
+                  <span class="mrel">=</span>
+                  <span class="mord">±</span>
+                  <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
+                    <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
+                  </span>
+                  <span class="vlist">
+                    <span class="" style="top: 0em;">
+                      <span class="fontsize-ensurer reset-size5 size5">
+                        <span class="" style="font-size: 1em;">​</span>
+                      </span>
+                      <span class="mord textstyle cramped">
+                        <span class="mord">
+                          <span class="mord mathit">a</span>
+                          <span class="msupsub">
+                            <span class="vlist">
+                              <span class="" style="top: -0.289em; margin-right: 0.05em;">
+                                <span class="fontsize-ensurer reset-size5 size5">
+                                  <span class="" style="font-size: 0em;">​</span>
+                                </span>
+                                <span class="reset-textstyle scriptstyle cramped">
+                                  <span class="mord mathrm">2</span>
+                                </span>
                               </span>
+                              <span class="baseline-fix">
+                                <span class="fontsize-ensurer reset-size5 size5">
+                                  <span class="" style="font-size: 0em;">​</span>
+                                </span>
+                              ​</span>
                             </span>
-                            <span class="baseline-fix">
-                              <span class="fontsize-ensurer reset-size5 size5">
-                                <span class="" style="font-size: 0em;">​</span>
-                              </span>
-                            ​</span>
                           </span>
                         </span>
-                      </span>
-                      <span class="mbin">+</span>
-                      <span class="mord">
-                        <span class="mord mathit">b</span>
-                        <span class="msupsub">
-                          <span class="vlist">
-                            <span class="" style="top: -0.289em; margin-right: 0.05em;">
-                              <span class="fontsize-ensurer reset-size5 size5">
-                                <span class="" style="font-size: 0em;">​</span>
-                              </span>
-                              <span class="reset-textstyle scriptstyle cramped">
-                                <span class="mord mathrm">2</span>
+                        <span class="mbin">+</span>
+                        <span class="mord">
+                          <span class="mord mathit">b</span>
+                          <span class="msupsub">
+                            <span class="vlist">
+                              <span class="" style="top: -0.289em; margin-right: 0.05em;">
+                                <span class="fontsize-ensurer reset-size5 size5">
+                                  <span class="" style="font-size: 0em;">​</span>
+                                </span>
+                                <span class="reset-textstyle scriptstyle cramped">
+                                  <span class="mord mathrm">2</span>
+                                </span>
                               </span>
+                              <span class="baseline-fix">
+                                <span class="fontsize-ensurer reset-size5 size5">
+                                  <span class="" style="font-size: 0em;">​</span>
+                                </span>
+                              ​</span>
                             </span>
-                            <span class="baseline-fix">
-                              <span class="fontsize-ensurer reset-size5 size5">
-                                <span class="" style="font-size: 0em;">​</span>
-                              </span>
-                            ​</span>
                           </span>
                         </span>
                       </span>
                     </span>
-                  </span>
-                  <span class="" style="top: -0.833389em;">
-                    <span class="fontsize-ensurer reset-size5 size5">
-                      <span class="" style="font-size: 1em;">​</span>
+                    <span class="" style="top: -0.833389em;">
+                      <span class="fontsize-ensurer reset-size5 size5">
+                        <span class="" style="font-size: 1em;">​</span>
+                      </span>
+                      <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
                     </span>
-                    <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
+                    <span class="baseline-fix">
+                      <span class="fontsize-ensurer reset-size5 size5">
+                        <span class="" style="font-size: 1em;">​</span>
+                      </span>
+                    ​</span>
                   </span>
-                  <span class="baseline-fix">
-                    <span class="fontsize-ensurer reset-size5 size5">
-                      <span class="" style="font-size: 1em;">​</span>
-                    </span>
-                  ​</span>
                 </span>
               </span>
             </span>
           </span>
-        </span>
-      HTML
+        HTML
 
-      output_gfm = html_to_gfm(html)
-      expect(output_gfm.strip).to eq(gfm.strip)
-    end
+        output_gfm = html_to_gfm(html)
+        expect(output_gfm.strip).to eq(gfm.strip)
+      end
 
-    verify(
-      'SanitizationFilter',
+      verify(
+        'SanitizationFilter',
 
-      <<-GFM.strip_heredoc
-      <a name="named-anchor"></a>
-      
-      <sub>sub</sub>
+        <<-GFM.strip_heredoc
+        <a name="named-anchor"></a>
 
-      <dl>
-        <dt>dt</dt>
-        <dd>dd</dd>
-      </dl>
+        <sub>sub</sub>
 
-      <kbd>kbd</kbd>
+        <dl>
+          <dt>dt</dt>
+          <dd>dd</dd>
+        </dl>
 
-      <q>q</q>
+        <kbd>kbd</kbd>
 
-      <samp>samp</samp>
+        <q>q</q>
 
-      <var>var</var>
+        <samp>samp</samp>
 
-      <ruby>ruby</ruby>
+        <var>var</var>
 
-      <rt>rt</rt>
+        <ruby>ruby</ruby>
 
-      <rp>rp</rp>
+        <rt>rt</rt>
 
-      <abbr>abbr</abbr>
-      GFM
-    )
+        <rp>rp</rp>
 
-    verify(
-      'SanitizationFilter',
+        <abbr>abbr</abbr>
 
-      <<-GFM.strip_heredoc,
-        ```
-        Plain text
-        ```
-      GFM
+        <summary>summary</summary>
 
-      <<-GFM.strip_heredoc,
-        ```ruby
-        def foo
-          bar
-        end
-        ```
-      GFM
+        <details>details</details>
+        GFM
+      )
+
+      verify(
+        'SanitizationFilter',
+
+        <<-GFM.strip_heredoc,
+          ```
+          Plain text
+          ```
+        GFM
+
+        <<-GFM.strip_heredoc,
+          ```ruby
+          def foo
+            bar
+          end
+          ```
+        GFM
+
+        <<-GFM.strip_heredoc
+          Foo
+
+              This is an example of GFM
+
+              ```js
+              Code goes here
+              ```
+        GFM
+      )
+
+      verify(
+        'MarkdownFilter',
+
+        "Line with two spaces at the end  \nto insert a linebreak",
+
+        '`code`',
+        '`` code with ` ticks ``',
+
+        '> Quote',
+
+        # multiline quote
+        <<-GFM.strip_heredoc,
+          > Multiline
+          > Quote
+          >
+          > With multiple paragraphs
+        GFM
+
+        '![Image](https://example.com/image.png)',
+
+        '# Heading with no anchor link',
+
+        '[Link](https://example.com)',
+
+        '- List item',
+
+        # multiline list item
+        <<-GFM.strip_heredoc,
+          - Multiline
+              List item
+        GFM
+
+        # nested lists
+        <<-GFM.strip_heredoc,
+          - Nested
+
+
+              - Lists
+        GFM
+
+        # list with blockquote
+        <<-GFM.strip_heredoc,
+          - List
+
+              > Blockquote
+        GFM
+
+        '1. Numbered list item',
+
+        # multiline numbered list item
+        <<-GFM.strip_heredoc,
+          1. Multiline
+              Numbered list item
+        GFM
+
+        # nested numbered list
+        <<-GFM.strip_heredoc,
+          1. Nested
 
-      <<-GFM.strip_heredoc
-        Foo
 
-            This is an example of GFM
+              1. Numbered lists
+        GFM
 
-            ```js
-            Code goes here
-            ```
-      GFM
-    )
+        '# Heading',
+        '## Heading',
+        '### Heading',
+        '#### Heading',
+        '##### Heading',
+        '###### Heading',
 
-    verify(
-      'MarkdownFilter',
+        '**Bold**',
 
-      "Line with two spaces at the end  \nto insert a linebreak",
+        '_Italics_',
 
-      '`code`',
-      '`` code with ` ticks ``',
+        '~~Strikethrough~~',
 
-      '> Quote',
+        '2^2',
 
-      # multiline quote
-      <<-GFM.strip_heredoc,
-        > Multiline
-        > Quote
-        >
-        > With multiple paragraphs
-      GFM
+        '-----',
 
-      '![Image](https://example.com/image.png)',
+        # table
+        <<-GFM.strip_heredoc,
+          | Centered | Right | Left |
+          |:--------:|------:|------|
+          | Foo | Bar | **Baz** |
+          | Foo | Bar | **Baz** |
+        GFM
 
-      '# Heading with no anchor link',
+        # table with empty heading
+        <<-GFM.strip_heredoc,
+          |  | x | y |
+          |---|---|---|
+          | a | 1 | 0 |
+          | b | 0 | 1 |
+        GFM
+      )
+    end
 
-      '[Link](https://example.com)',
+    alias_method :gfm_to_html, :markdown
 
-      '- List item',
+    def verify(label, *gfms)
+      aggregate_failures(label) do
+        gfms.each do |gfm|
+          html = gfm_to_html(gfm)
+          output_gfm = html_to_gfm(html)
+          expect(output_gfm.strip).to eq(gfm.strip)
+        end
+      end
+    end
 
-      # multiline list item
-      <<-GFM.strip_heredoc,
-        - Multiline
-            List item
-      GFM
+    # Fake a `current_user` helper
+    def current_user
+      @feat.user
+    end
+  end
 
-      # nested lists
-      <<-GFM.strip_heredoc,
-        - Nested
+  describe 'Copying code' do
+    let(:project) { create(:project) }
 
+    context 'from a diff' do
+      before do
+        visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+      end
 
-            - Lists
-      GFM
+      context 'selecting one word of text' do
+        it 'copies as inline code' do
+          verify(
+            '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
 
-      # list with blockquote
-      <<-GFM.strip_heredoc,
-        - List
+            '`RuntimeError`'
+          )
+        end
+      end
 
-            > Blockquote
-      GFM
+      context 'selecting one line of text' do
+        it 'copies as inline code' do
+          verify(
+            '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
 
-      '1. Numbered list item',
+            '`raise RuntimeError, "System commands must be given as an array of strings"`'
+          )
+        end
+      end
 
-      # multiline numbered list item
-      <<-GFM.strip_heredoc,
-        1. Multiline
-            Numbered list item
-      GFM
+      context 'selecting multiple lines of text' do
+        it 'copies as a code block' do
+          verify(
+            '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+            <<-GFM.strip_heredoc,
+              ```ruby
+                    raise RuntimeError, "System commands must be given as an array of strings"
+                  end
+              ```
+            GFM
+          )
+        end
+      end
+    end
 
-      # nested numbered list
-      <<-GFM.strip_heredoc,
-        1. Nested
+    context 'from a blob' do
+      before do
+        visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+      end
 
+      context 'selecting one word of text' do
+        it 'copies as inline code' do
+          verify(
+            '.line[id="LC9"] .no',
 
-            1. Numbered lists
-      GFM
+            '`RuntimeError`'
+          )
+        end
+      end
 
-      '# Heading',
-      '## Heading',
-      '### Heading',
-      '#### Heading',
-      '##### Heading',
-      '###### Heading',
+      context 'selecting one line of text' do
+        it 'copies as inline code' do
+          verify(
+            '.line[id="LC9"]',
 
-      '**Bold**',
+            '`raise RuntimeError, "System commands must be given as an array of strings"`'
+          )
+        end
+      end
 
-      '_Italics_',
+      context 'selecting multiple lines of text' do
+        it 'copies as a code block' do
+          verify(
+            '.line[id="LC9"], .line[id="LC10"]',
+
+            <<-GFM.strip_heredoc,
+              ```ruby
+                    raise RuntimeError, "System commands must be given as an array of strings"
+                  end
+              ```
+            GFM
+          )
+        end
+      end
+    end
 
-      '~~Strikethrough~~',
+    context 'from a GFM code block' do
+      before do
+        visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+      end
 
-      '2^2',
+      context 'selecting one word of text' do
+        it 'copies as inline code' do
+          verify(
+            '.line[id="LC27"] .s2',
 
-      '-----',
+            '`"bio"`'
+          )
+        end
+      end
 
-      # table
-      <<-GFM.strip_heredoc,
-        | Centered | Right | Left |
-        |:--------:|------:|------|
-        | Foo | Bar | **Baz** |
-        | Foo | Bar | **Baz** |
-      GFM
+      context 'selecting one line of text' do
+        it 'copies as inline code' do
+          verify(
+            '.line[id="LC27"]',
 
-      # table with empty heading
-      <<-GFM.strip_heredoc,
-        |  | x | y |
-        |---|---|---|
-        | a | 1 | 0 |
-        | b | 0 | 1 |
-      GFM
-    )
+            '`"bio": null,`'
+          )
+        end
+      end
+
+      context 'selecting multiple lines of text' do
+        it 'copies as a code block with the correct language' do
+          verify(
+            '.line[id="LC27"], .line[id="LC28"]',
+
+            <<-GFM.strip_heredoc,
+              ```json
+                  "bio": null,
+                  "skype": "",
+              ```
+            GFM
+          )
+        end
+      end
+    end
+
+    def verify(selector, gfm)
+      html = html_for_selector(selector)
+      output_gfm = html_to_gfm(html, 'transformCodeSelection')
+      expect(output_gfm.strip).to eq(gfm.strip)
+    end
   end
 
-  alias_method :gfm_to_html, :markdown
+  def html_for_selector(selector)
+    js = <<-JS.strip_heredoc
+      (function(selector) {
+        var els = document.querySelectorAll(selector);
+        var htmls = _.map(els, function(el) { return el.outerHTML; });
+        return htmls.join("\\n");
+      })("#{escape_javascript(selector)}")
+    JS
+    page.evaluate_script(js)
+  end
 
-  def html_to_gfm(html)
+  def html_to_gfm(html, transformer = 'transformGFMSelection')
     js = <<-JS.strip_heredoc
       (function(html) {
+        var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+
         var node = document.createElement('div');
         node.innerHTML = html;
+
+        node = transformer(node);
+        if (!node) return null;
+
         return window.gl.CopyAsGFM.nodeToGFM(node);
       })("#{escape_javascript(html)}")
     JS
     page.evaluate_script(js)
   end
-
-  def verify(label, *gfms)
-    aggregate_failures(label) do
-      gfms.each do |gfm|
-        html = gfm_to_html(gfm)
-        output_gfm = html_to_gfm(html)
-        expect(output_gfm.strip).to eq(gfm.strip)
-      end
-    end
-  end
-
-  # Fake a `current_user` helper
-  def current_user
-    @feat.user
-  end
 end
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c977f2662967bd4c548884091513d1b919850dcd
--- /dev/null
+++ b/spec/features/dashboard/activity_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Activity', feature: true do
+  before do
+    login_as(create :user)
+    visit activity_dashboard_path
+  end
+  
+  it_behaves_like "it has an RSS button with current_user's private token"
+  it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index 038c1641be9cacd664f84aa9c85de77267517077..f33bcbb5318b362be36fb0579e84bb70b6ad27f6 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -25,4 +25,19 @@ RSpec.describe 'Dashboard Archived Project', feature: true do
     expect(page).to have_link(project.name)
     expect(page).to have_link(archived_project.name)
   end
+
+  it 'searchs archived projects', :js do
+    click_button 'Last updated'
+    click_link 'Show archived projects'
+
+    expect(page).to have_link(project.name)
+    expect(page).to have_link(archived_project.name)
+
+    fill_in 'project-filter-form-field', with: archived_project.name
+
+    find('#project-filter-form-field').native.send_keys :return
+
+    expect(page).not_to have_link(project.name)
+    expect(page).to have_link(archived_project.name)
+  end
 end
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca04107d33a7a931a90eb0c0567b036bad37b936
--- /dev/null
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'Dashboard Groups page', js: true, feature: true do
+  include WaitForAjax
+
+  let!(:user) { create :user }
+  let!(:group) { create(:group) }
+  let!(:nested_group) { create(:group, :nested) }
+  let!(:another_group) { create(:group) }
+
+  before do
+    group.add_owner(user)
+    nested_group.add_owner(user)
+
+    login_as(user)
+
+    visit dashboard_groups_path
+  end
+
+  it 'shows groups user is member of' do
+    expect(page).to have_content(group.full_name)
+    expect(page).to have_content(nested_group.full_name)
+    expect(page).not_to have_content(another_group.full_name)
+  end
+
+  it 'filters groups' do
+    fill_in 'filter_groups', with: group.name
+    wait_for_ajax
+
+    expect(page).to have_content(group.full_name)
+    expect(page).not_to have_content(nested_group.full_name)
+    expect(page).not_to have_content(another_group.full_name)
+  end
+
+  it 'resets search when user cleans the input' do
+    fill_in 'filter_groups', with: group.name
+    wait_for_ajax
+
+    fill_in 'filter_groups', with: ""
+    wait_for_ajax
+
+    expect(page).to have_content(group.full_name)
+    expect(page).to have_content(nested_group.full_name)
+    expect(page).not_to have_content(another_group.full_name)
+    expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+  end
+end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 2db1cf71209ba2e1ef7ba00915ed4e2f1d49914a..f4420814c3a1bc94b951408c13e312f44d40a749 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -45,4 +45,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
     expect(page).to have_content(assigned_issue.title)
     expect(page).to have_content(other_issue.title)
   end
+
+  it_behaves_like "it has an RSS button with current_user's private token"
+  it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
 end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index ba77093a6d4b56f3f4d322dfba1be4de4fa77085..49d93db58a930faaa8d53a89851c362f36eaffca 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -12,7 +12,7 @@ feature 'Project member activity', feature: true, js: true do
 
   def visit_activities_and_wait_with_event(event_type)
     Event.create(project: project, author_id: user.id, action: event_type)
-    visit activity_namespace_project_path(project.namespace.path, project.path)
+    visit activity_namespace_project_path(project.namespace, project)
     wait_for_ajax
   end
 
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c4e58d14f75beb11f1af3c3e0cf2cf2ef677b558
--- /dev/null
+++ b/spec/features/dashboard/projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Projects', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, name: "awesome stuff") }
+
+  before do
+    project.team << [user, :developer]
+    login_as user
+    visit dashboard_projects_path
+  end
+
+  it 'shows the project the user in a member of in the list' do
+    visit dashboard_projects_path
+    expect(page).to have_content('awesome stuff')
+  end
+
+  describe "with a pipeline" do
+    let(:pipeline) {  create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+    before do
+      pipeline
+    end
+
+    it 'shows that the last pipeline passed' do
+      visit dashboard_projects_path
+      expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
+    end
+  end
+
+  it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+end
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index c2e0612aef83dae64e379df9fbcbc8ebaee229cf..34d6257f5fd77b217b016a75700e2e63fd108f99 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -1,26 +1,45 @@
 require 'spec_helper'
 
-describe "Dashboard > User filters projects", feature: true do
+describe 'Dashboard > User filters projects', :feature do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+  let(:user2) { create(:user) }
+  let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+
+  before do
+    project.team << [user, :master]
+
+    login_as(user)
+  end
+
   describe 'filtering personal projects' do
     before do
-      user = create(:user)
-      project = create(:project, name: "Victorialand", namespace: user.namespace)
-      project.team << [user, :master]
-
-      user2 = create(:user)
-      project2 = create(:project, name: "Treasure", namespace: user2.namespace)
       project2.team << [user, :developer]
 
-      login_as(user)
       visit dashboard_projects_path
     end
 
     it 'filters by projects "Owned by me"' do
-      click_link "Owned by me"
+      click_link 'Owned by me'
 
       expect(page).to have_css('.is-active', text: 'Owned by me')
       expect(page).to have_content('Victorialand')
       expect(page).not_to have_content('Treasure')
     end
   end
+
+  describe 'filtering starred projects', :js do
+    before do
+      user.toggle_star(project)
+
+      visit dashboard_projects_path
+    end
+
+    it 'returns message when starred projects fitler returns no results' do
+      fill_in 'project-filter-form-field', with: 'Beta\n'
+
+      expect(page).to have_content('No projects found')
+      expect(page).not_to have_content('You don\'t have starred projects yet')
+    end
+  end
 end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index b898f9bc64fb3ca7c065b7df443c1479262751b8..8c61cdebc4b88eb4a3bdf95aee91f902d9c0d794 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -48,10 +48,10 @@ describe "Dashboard Issues filtering", feature: true, js: true do
     it 'updates atom feed link' do
       visit_issues(milestone_title: '', assignee_id: user.id)
 
-      link = find('.nav-controls a', text: 'Subscribe')
-      params = CGI::parse(URI.parse(link[:href]).query)
+      link = find('.nav-controls a[title="Subscribe"]')
+      params = CGI.parse(URI.parse(link[:href]).query)
       auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
-      auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
+      auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
 
       expect(params).to include('private_token' => [user.private_token])
       expect(params).to include('milestone_title' => [''])
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..773ae4b38bc9faecb8a0afa98cde52c9c3e6fd11
--- /dev/null
+++ b/spec/features/explore/groups_list_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'Explore Groups page', js: true, feature: true do
+  include WaitForAjax
+
+  let!(:user) { create :user }
+  let!(:group) { create(:group) }
+  let!(:public_group) { create(:group, :public) }
+  let!(:private_group) { create(:group, :private) }
+
+  before do
+    group.add_owner(user)
+
+    login_as(user)
+
+    visit explore_groups_path
+  end
+
+  it 'shows groups user is member of' do
+    expect(page).to have_content(group.full_name)
+    expect(page).to have_content(public_group.full_name)
+    expect(page).not_to have_content(private_group.full_name)
+  end
+
+  it 'filters groups' do
+    fill_in 'filter_groups', with: group.name
+    wait_for_ajax
+
+    expect(page).to have_content(group.full_name)
+    expect(page).not_to have_content(public_group.full_name)
+    expect(page).not_to have_content(private_group.full_name)
+  end
+
+  it 'resets search when user cleans the input' do
+    fill_in 'filter_groups', with: group.name
+    wait_for_ajax
+
+    fill_in 'filter_groups', with: ""
+    wait_for_ajax
+
+    expect(page).to have_content(group.full_name)
+    expect(page).to have_content(public_group.full_name)
+    expect(page).not_to have_content(private_group.full_name)
+    expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+  end
+end
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3b481cba424fd68a11e88c535b45943c1d572fe0
--- /dev/null
+++ b/spec/features/groups/activity_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+feature 'Group activity page', feature: true do
+  let(:group) { create(:group) }
+  let(:path) { activity_group_path(group) }
+
+  context 'when signed in' do
+    before do
+      user = create(:group_member, :developer, user: create(:user), group: group ).user
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button with current_user's private token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button without a private token"
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8528718a2f751cf1aa3e33f6f72595babe0ac18f
--- /dev/null
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Group name toggle', feature: true, js: true do
+  let(:group) { create(:group) }
+  let(:nested_group_1) { create(:group, parent: group) }
+  let(:nested_group_2) { create(:group, parent: nested_group_1) }
+  let(:nested_group_3) { create(:group, parent: nested_group_2) }
+
+  before do
+    login_as :user
+  end
+
+  it 'is not present for less than 3 groups' do
+    visit group_path(group)
+    expect(page).not_to have_css('.group-name-toggle')
+
+    visit group_path(nested_group_1)
+    expect(page).not_to have_css('.group-name-toggle')
+  end
+
+  it 'is present for nested group of 3 or more in the namespace' do
+    visit group_path(nested_group_2)
+    expect(page).to have_css('.group-name-toggle')
+
+    visit group_path(nested_group_3)
+    expect(page).to have_css('.group-name-toggle')
+  end
+
+  context 'for group with at least 3 groups' do
+    before do
+      visit group_path(nested_group_2)
+    end
+
+    it 'should show the full group namespace when toggled' do
+      expect(page).not_to have_content(group.name)
+      expect(page).to have_css('.group-path.hidable', visible: false)
+
+      click_button '...'
+
+      expect(page).to have_content(group.name)
+      expect(page).to have_css('.group-path.hidable', visible: true)
+    end
+  end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 476eca17a9d8e36c3ee7bad6679ebf98febc434f..1b3747c390bca48abc335605fe9bc246e23550cc 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -5,4 +5,22 @@ feature 'Group issues page', feature: true do
   let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
 
   include_examples 'project features apply to issuables', Issue
+
+  context 'rss feed' do
+    let(:access_level) { ProjectFeature::ENABLED }
+
+    context 'when signed in' do
+      let(:user) { user_in_group }
+
+      it_behaves_like "it has an RSS button with current_user's private token"
+      it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+    end
+
+    context 'when signed out' do
+      let(:user) { nil }
+
+      it_behaves_like "it has an RSS button without a private token"
+      it_behaves_like "an autodiscoverable RSS feed without a private token"
+    end
+  end
 end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fb39693e8ca90fe30b6306ae8a5d52635493aa79
--- /dev/null
+++ b/spec/features/groups/show_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Group show page', feature: true do
+  let(:group) { create(:group) }
+  let(:path) { group_path(group) }
+
+  context 'when signed in' do
+    before do
+      user = create(:group_member, :developer, user: create(:user), group: group ).user
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 37b7c20239fcf2a3b3c8f38e7cb56d93534dc564..d243f9478bb075147098b51776dfd2d6feaaeb4d 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -43,6 +43,44 @@ feature 'Group', feature: true do
         expect(page).to have_namespace_error_message
       end
     end
+
+    describe 'Mattermost team creation' do
+      before do
+        allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled)
+
+        visit new_group_path
+      end
+
+      context 'Mattermost enabled' do
+        let(:mattermost_enabled) { true }
+
+        it 'displays a team creation checkbox' do
+          expect(page).to have_selector('#group_create_chat_team')
+        end
+
+        it 'checks the checkbox by default' do
+          expect(find('#group_create_chat_team')['checked']).to eq(true)
+        end
+
+        it 'updates the team URL on graph path update', :js do
+          out_span = find('span[data-bind-out="create_chat_team"]')
+
+          expect(out_span.text).to be_empty
+
+          fill_in('group_path', with: 'test-group')
+
+          expect(out_span.text).to eq('test-group')
+        end
+      end
+
+      context 'Mattermost disabled' do
+        let(:mattermost_enabled) { false }
+
+        it 'doesnt show a team creation checkbox if Mattermost not enabled' do
+          expect(page).not_to have_selector('#group_create_chat_team')
+        end
+      end
+    end
   end
 
   describe 'create a nested group' do
@@ -105,7 +143,7 @@ feature 'Group', feature: true do
 
       visit path
 
-      expect(page).to have_css('.group-home-desc > p > img')
+      expect(page).to have_css('.group-home-desc > p > gl-emoji')
     end
 
     it 'sanitizes unwanted tags' do
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 40a1fced8d837cacc8bd42e91940e3cb732e52c6..e0b2404e60ae8afc166087d087ca3d137d3a1e23 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -33,4 +33,30 @@ describe 'Help Pages', feature: true do
       it_behaves_like 'help page', prefix: '/gitlab'
     end
   end
+
+  context 'in a production environment with version check enabled', js: true do
+    before do
+      allow(Rails.env).to receive(:production?) { true }
+      allow(current_application_settings).to receive(:version_check_enabled) { true }
+      allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' }
+
+      login_as :user
+      visit help_path
+    end
+
+    it 'should display a version check image' do
+      expect(find('.js-version-status-badge')).to be_visible
+    end
+
+    it 'should have a src url' do
+      expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/)
+    end
+
+    it 'should hide the version check image if the image request fails' do
+      # We use '--load-images=no' with poltergeist so we must trigger manually
+      execute_script("$('.js-version-status-badge').trigger('error');")
+
+      expect(find('.js-version-status-badge', visible: false)).not_to be_visible
+    end
+  end
 end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 73553f97d6f399ee20a11137dabe9e33c96b95bc..bfe43bff10fe8c1a1c4a30e2d414aaf2f13faf49 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
   end
 
   def selected_sort_order
-    find('.pull-right .dropdown button').text.downcase
+    find('.filter-dropdown-container .dropdown button').text.downcase
   end
 
   def visit_merge_requests_with_state(project, state)
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index e31bc40adc3e3d5666a93d582a81659f3a5fa249..b90bf6268fdfc638b039d5a45104ee1d21f0d1fc 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -30,6 +30,13 @@ describe 'issuable list', feature: true do
     end
   end
 
+  it "counts merge requests closing issues icons for each issue" do
+    visit_issuable_list(:issue)
+
+    expect(page).to have_selector('.icon-merge-request-unmerged', count: 1)
+    expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1)
+  end
+
   def visit_issuable_list(issuable_type)
     if issuable_type == :issue
       visit namespace_project_issues_path(project.namespace, project)
@@ -40,11 +47,12 @@ describe 'issuable list', feature: true do
 
   def create_issuables(issuable_type)
     3.times do
-      if issuable_type == :issue
-        issuable = create(:issue, project: project, author: user)
-      else
-        issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
-      end
+      issuable =
+        if issuable_type == :issue
+          create(:issue, project: project, author: user)
+        else
+          create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+        end
 
       2.times do
         create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
@@ -53,5 +61,15 @@ describe 'issuable list', feature: true do
       create(:award_emoji, :downvote, awardable: issuable)
       create(:award_emoji, :upvote, awardable: issuable)
     end
+
+    if issuable_type == :issue
+      issue = Issue.reorder(:iid).first
+      merge_request = create(:merge_request,
+                              title: FFaker::Lorem.sentence,
+                              source_project: project,
+                              source_branch: FFaker::Name.name)
+
+      MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+    end
   end
 end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 73e43316dc7ff8eafe0fa45b46f1589dface9668..16e453bc3286de12def772e3a5868a24c8a6089d 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -17,22 +17,35 @@ describe 'Awards Emoji', feature: true do
       login_as(user)
     end
 
+    describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
+      before do
+        # The `heart_tip` emoji is not valid anymore so we need to skip validation
+        issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
+        visit namespace_project_issue_path(project.namespace, project, issue)
+      end
+
+      # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
+      it 'does not shows a 500 page' do
+        expect(page).to have_text(issue.title)
+      end
+    end
+
     describe 'Click award emoji from issue#show' do
-      let!(:note) {  create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+      let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
 
       before do
         visit namespace_project_issue_path(project.namespace, project, issue)
       end
 
       it 'increments the thumbsdown emoji', js: true do
-        find('[data-emoji="thumbsdown"]').click
+        find('[data-name="thumbsdown"]').click
         wait_for_ajax
         expect(thumbsdown_emoji).to have_text("1")
       end
 
       context 'click the thumbsup emoji' do
         it 'increments the thumbsup emoji', js: true do
-          find('[data-emoji="thumbsup"]').click
+          find('[data-name="thumbsup"]').click
           wait_for_ajax
           expect(thumbsup_emoji).to have_text("1")
         end
@@ -44,7 +57,7 @@ describe 'Awards Emoji', feature: true do
 
       context 'click the thumbsdown emoji' do
         it 'increments the thumbsdown emoji', js: true do
-          find('[data-emoji="thumbsdown"]').click
+          find('[data-name="thumbsdown"]').click
           wait_for_ajax
           expect(thumbsdown_emoji).to have_text("1")
         end
@@ -67,6 +80,18 @@ describe 'Awards Emoji', feature: true do
           expect(page).not_to have_selector(emoji_counter)
         end
       end
+
+      context 'execute /award slash command' do
+        it 'toggles the emoji award on noteable', js: true do
+          execute_slash_command('/award :100:')
+
+          expect(find(noteable_award_counter)).to have_text("1")
+
+          execute_slash_command('/award :100:')
+
+          expect(page).not_to have_selector(noteable_award_counter)
+        end
+      end
     end
   end
 
@@ -80,6 +105,15 @@ describe 'Awards Emoji', feature: true do
     end
   end
 
+  def execute_slash_command(cmd)
+    within('.js-main-target-form') do
+      fill_in 'note[note]', with: cmd
+      click_button 'Comment'
+    end
+
+    wait_for_ajax
+  end
+
   def thumbsup_emoji
     page.all(emoji_counter).first
   end
@@ -92,15 +126,19 @@ describe 'Awards Emoji', feature: true do
     'span.js-counter'
   end
 
+  def noteable_award_counter
+    ".awards .active"
+  end
+
   def toggle_smiley_emoji(status)
     within('.note') do
       find('.note-emoji-button').click
     end
 
     unless status
-      first('[data-emoji="smiley"]').click
+      first('[data-name="smiley"]').click
     else
-      find('[data-emoji="smiley"]').click
+      find('[data-name="smiley"]').click
     end
 
     wait_for_ajax
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 832757b24d4f6fd71dee14fbf6346ec67e5e61cf..2f59630b4fbbac895a15249200a8c234931c4bda 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -55,7 +55,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
         context 'to all issues' do
           before do
             check 'check_all_issues'
-            open_labels_dropdown ['bug', 'feature']
+            open_labels_dropdown %w(bug feature)
             update_issues
           end
 
@@ -70,7 +70,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
         context 'to a issue' do
           before do
             check "selected_issue_#{issue1.id}"
-            open_labels_dropdown ['bug', 'feature']
+            open_labels_dropdown %w(bug feature)
             update_issues
           end
 
@@ -112,7 +112,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
           visit namespace_project_issues_path(project.namespace, project)
 
           check 'check_all_issues'
-          unmark_labels_in_dropdown ['bug', 'feature']
+          unmark_labels_in_dropdown %w(bug feature)
           update_issues
         end
 
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 762cab0c0e12921c16fc7b8e9674d4cf0ad67543..572bca3de21ee71776deeea3ecb596ee8b4da842 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,76 +1,93 @@
 require 'rails_helper'
 
-feature 'Resolving all open discussions in a merge request from an issue', feature: true do
+feature 'Resolving all open discussions in a merge request from an issue', feature: true, js: true do
   let(:user) { create(:user) }
-  let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+  let(:project) { create(:project) }
   let(:merge_request) { create(:merge_request, source_project: project) }
   let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
 
-  before do
-    project.team << [user, :master]
-    login_as user
-  end
-
-  context 'with the internal tracker disabled' do
+  describe 'as a user with access to the project' do
     before do
-      project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+      project.team << [user, :master]
+      login_as user
       visit namespace_project_merge_request_path(project.namespace, project, merge_request)
     end
 
-    it 'does not show a link to create a new issue' do
-      expect(page).not_to have_link 'open an issue to resolve them later'
-    end
-  end
-
-  context 'merge request has discussions that need to be resolved' do
-    before do
-      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    it 'shows a button to resolve all discussions by creating a new issue' do
+      within('li#resolve-count-app') do
+        expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+      end
     end
 
-    it 'shows a warning that the merge request contains unresolved discussions' do
-      expect(page).to have_content 'This merge request has unresolved discussions'
-    end
+    context 'resolving the discussion' do
+      before do
+        click_button 'Resolve discussion'
+      end
 
-    it 'has a link to resolve all discussions by creating an issue' do
-      page.within '.mr-widget-body' do
-        expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+      it 'hides the link for creating a new issue' do
+        expect(page).not_to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
       end
     end
 
     context 'creating an issue for discussions' do
       before do
-        page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+        click_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
       end
 
-      it 'shows an issue with the title filled in' do
-        title_field = page.find_field('issue[title]')
+      it_behaves_like 'creating an issue for a discussion'
+    end
 
-        expect(title_field.value).to include(merge_request.title)
+    context 'for a project where all discussions need to be resolved before merging' do
+      before do
+        project.update_attribute(:only_allow_merge_if_all_discussions_are_resolved, true)
       end
 
-      it 'has a mention of the discussion in the description'  do
-        description_field = page.find_field('issue[description]')
+      context 'with the internal tracker disabled' do
+        before do
+          project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+          visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+        end
 
-        expect(description_field.value).to include(discussion.first_note.note)
+        it 'does not show a link to create a new issue' do
+          expect(page).not_to have_link 'open an issue to resolve them later'
+        end
       end
 
-      it 'has a hidden field for the merge request' do
-        merge_request_field = find('#merge_request_for_resolving_discussions', visible: false)
-
-        expect(merge_request_field.value).to eq(merge_request.iid.to_s)
-      end
+      context 'merge request has discussions that need to be resolved' do
+        before do
+          visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+        end
 
-      it 'can create a new issue for the project' do
-        expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
-      end
+        it 'shows a warning that the merge request contains unresolved discussions' do
+          expect(page).to have_content 'This merge request has unresolved discussions'
+        end
 
-      it 'resolves the discussion in the merge request' do
-        click_button 'Submit issue'
+        it 'has a link to resolve all discussions by creating an issue' do
+          page.within '.mr-widget-body' do
+            expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+          end
+        end
 
-        discussion.first_note.reload
+        context 'creating an issue for discussions' do
+          before do
+            page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+          end
 
-        expect(discussion.resolved?).to eq(true)
+          it_behaves_like 'creating an issue for a discussion'
+        end
       end
     end
   end
+
+  describe 'as a reporter' do
+    before do
+      project.team << [user, :reporter]
+      login_as user
+      visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+    end
+
+    it 'Shows a notice to ask someone else to resolve the discussions' do
+      expect(page).to have_content("The discussions at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.")
+    end
+  end
 end
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..88e2cc60d79f0c0a1151d834128f5bf1390250fa
--- /dev/null
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+feature 'Resolve an open discussion in a merge request by creating an issue', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+  let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+
+  describe 'As a user with access to the project' do
+    before do
+      project.team << [user, :master]
+      login_as user
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    context 'with the internal tracker disabled' do
+      before do
+        project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+        visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+      end
+
+      it 'does not show a link to create a new issue' do
+        expect(page).not_to have_link 'Resolve this discussion in a new issue'
+      end
+    end
+
+    context 'resolving the discussion', js: true do
+      before do
+        click_button 'Resolve discussion'
+      end
+
+      it 'hides the link for creating a new issue' do
+        expect(page).not_to have_link 'Resolve this discussion in a new issue'
+      end
+
+      it 'shows the link for creating a new issue when unresolving a discussion' do
+        page.within '.diff-content' do
+          click_button 'Unresolve discussion'
+        end
+
+        expect(page).to have_link 'Resolve this discussion in a new issue'
+      end
+    end
+
+    it 'has a link to create a new issue for a discussion' do
+      new_issue_link = new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+
+      expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link
+    end
+
+    context 'creating the issue' do
+      before do
+        click_link 'Resolve this discussion in a new issue', href: new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+      end
+
+      it 'has a hidden field for the discussion' do
+        discussion_field = find('#discussion_to_resolve', visible: false)
+
+        expect(discussion_field.value).to eq(discussion.id.to_s)
+      end
+
+      it_behaves_like 'creating an issue for a discussion'
+    end
+  end
+
+  describe 'as a reporter' do
+    before do
+      project.team << [user, :reporter]
+      login_as user
+      visit new_namespace_project_issue_path(project.namespace, project,
+                                             merge_request_to_resolve_discussions_of: merge_request.iid,
+                                             discussion_to_resolve: discussion.id)
+    end
+
+    it 'Shows a notice to ask someone else to resolve the discussions' do
+      expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
+                                   "(discussion #{discussion.first_note.id}) will stay unresolved."\
+                                   "Ask someone with permission to resolve it.")
+    end
+  end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 93763f092fb21e75758ca217fbc633cecdde7b44..4dcc56a97d14678de5215447550dd7a61141a68a 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -1,6 +1,7 @@
 require 'rails_helper'
 
-describe 'Dropdown assignee', js: true, feature: true do
+describe 'Dropdown assignee', :feature, :js do
+  include FilteredSearchHelpers
   include WaitForAjax
 
   let!(:project) { create(:empty_project) }
@@ -9,17 +10,10 @@ describe 'Dropdown assignee', js: true, feature: true do
   let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
   let(:filtered_search) { find('.filtered-search') }
   let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
-
-  def send_keys_to_filtered_search(input)
-    input.split("").each do |i|
-      filtered_search.send_keys(i)
-      sleep 5
-      wait_for_ajax
-    end
-  end
+  let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
 
   def dropdown_assignee_size
-    page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
+    filter_dropdown.all('.filter-dropdown-item').size
   end
 
   def click_assignee(text)
@@ -56,63 +50,80 @@ describe 'Dropdown assignee', js: true, feature: true do
     end
 
     it 'should hide loading indicator when loaded' do
-      send_keys_to_filtered_search('assignee:')
+      filtered_search.set('assignee:')
 
-      expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
+      expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
+      expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
     end
 
     it 'should load all the assignees when opened' do
-      send_keys_to_filtered_search('assignee:')
+      filtered_search.set('assignee:')
 
       expect(dropdown_assignee_size).to eq(3)
     end
 
     it 'shows current user at top of dropdown' do
-      send_keys_to_filtered_search('assignee:')
+      filtered_search.set('assignee:')
 
-      expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name)
+      expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
     end
   end
 
   describe 'filtering' do
     before do
-      send_keys_to_filtered_search('assignee:')
+      filtered_search.set('assignee:')
+
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
     end
 
     it 'filters by name' do
-      send_keys_to_filtered_search('j')
+      filtered_search.send_keys('j')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
     end
 
     it 'filters by case insensitive name' do
-      send_keys_to_filtered_search('J')
+      filtered_search.send_keys('J')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
     end
 
     it 'filters by username with symbol' do
-      send_keys_to_filtered_search('@ot')
+      filtered_search.send_keys('@ot')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
     end
 
     it 'filters by case insensitive username with symbol' do
-      send_keys_to_filtered_search('@OT')
+      filtered_search.send_keys('@OT')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
     end
 
     it 'filters by username without symbol' do
-      send_keys_to_filtered_search('ot')
+      filtered_search.send_keys('ot')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
     end
 
     it 'filters by case insensitive username without symbol' do
-      send_keys_to_filtered_search('OT')
+      filtered_search.send_keys('OT')
 
-      expect(dropdown_assignee_size).to eq(2)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+      expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
     end
   end
 
@@ -125,22 +136,25 @@ describe 'Dropdown assignee', js: true, feature: true do
       click_assignee(user_jacob.name)
 
       expect(page).to have_css(js_dropdown_assignee, visible: false)
-      expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
+      expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the assignee username when the assignee has been filtered' do
-      send_keys_to_filtered_search('roo')
+      filtered_search.send_keys('roo')
       click_assignee(user.name)
 
       expect(page).to have_css(js_dropdown_assignee, visible: false)
-      expect(filtered_search.value).to eq("assignee:@#{user.username} ")
+      expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'selects `no assignee`' do
       find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
 
       expect(page).to have_css(js_dropdown_assignee, visible: false)
-      expect(filtered_search.value).to eq("assignee:none ")
+      expect_tokens([{ name: 'assignee', value: 'none' }])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -173,7 +187,7 @@ describe 'Dropdown assignee', js: true, feature: true do
   describe 'caching requests' do
     it 'caches requests after the first load' do
       filtered_search.set('assignee')
-      send_keys_to_filtered_search(':')
+      filtered_search.send_keys(':')
       initial_size = dropdown_assignee_size
 
       expect(initial_size).to be > 0
@@ -182,7 +196,7 @@ describe 'Dropdown assignee', js: true, feature: true do
       project.team << [new_user, :master]
       find('.filtered-search-input-container .clear-search').click
       filtered_search.set('assignee')
-      send_keys_to_filtered_search(':')
+      filtered_search.send_keys(':')
 
       expect(dropdown_assignee_size).to eq(initial_size)
     end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 59e302f0e2d10a0938ef892cbb558f4323f9142d..1772a1200452753a52c93eaef86b85f597b45985 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,7 @@
 require 'rails_helper'
 
 describe 'Dropdown author', js: true, feature: true do
+  include FilteredSearchHelpers
   include WaitForAjax
 
   let!(:project) { create(:empty_project) }
@@ -13,9 +14,10 @@ describe 'Dropdown author', js: true, feature: true do
   def send_keys_to_filtered_search(input)
     input.split("").each do |i|
       filtered_search.send_keys(i)
-      sleep 5
-      wait_for_ajax
     end
+
+    sleep 0.5
+    wait_for_ajax
   end
 
   def dropdown_author_size
@@ -121,14 +123,16 @@ describe 'Dropdown author', js: true, feature: true do
       click_author(user_jacob.name)
 
       expect(page).to have_css(js_dropdown_author, visible: false)
-      expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
+      expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the author username when the author has been filtered' do
       click_author(user.name)
 
       expect(page).to have_css(js_dropdown_author, visible: false)
-      expect(filtered_search.value).to eq("author:@#{user.username} ")
+      expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
     end
   end
 
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 04dd54ab45937e632640cd0ab815daab5baedec7..01b657bcadaf7b6f102f040a1da898199395e0ac 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -1,6 +1,7 @@
 require 'rails_helper'
 
 describe 'Dropdown hint', js: true, feature: true do
+  include FilteredSearchHelpers
   include WaitForAjax
 
   let!(:project) { create(:empty_project) }
@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-author', visible: true)
-      expect(filtered_search.value).to eq('author:')
+      expect_tokens([{ name: 'author' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the assignee dropdown when you click on assignee' do
@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-assignee', visible: true)
-      expect(filtered_search.value).to eq('assignee:')
+      expect_tokens([{ name: 'assignee' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the milestone dropdown when you click on milestone' do
@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-milestone', visible: true)
-      expect(filtered_search.value).to eq('milestone:')
+      expect_tokens([{ name: 'milestone' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the label dropdown when you click on label' do
@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-label', visible: true)
-      expect(filtered_search.value).to eq('label:')
+      expect_tokens([{ name: 'label' }])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-author', visible: true)
-      expect(filtered_search.value).to eq('author:')
+      expect_tokens([{ name: 'author' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the assignee dropdown when you click on assignee' do
@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-assignee', visible: true)
-      expect(filtered_search.value).to eq('assignee:')
+      expect_tokens([{ name: 'assignee' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the milestone dropdown when you click on milestone' do
@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-milestone', visible: true)
-      expect(filtered_search.value).to eq('milestone:')
+      expect_tokens([{ name: 'milestone' }])
+      expect_filtered_search_input_empty
     end
 
     it 'opens the label dropdown when you click on label' do
@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
 
       expect(page).to have_css(js_dropdown_hint, visible: false)
       expect(page).to have_css('#js-dropdown-label', visible: true)
-      expect(filtered_search.value).to eq('label:')
+      expect_tokens([{ name: 'label' }])
+      expect_filtered_search_input_empty
+    end
+  end
+
+  describe 'reselecting from dropdown' do
+    it 'reuses existing author text' do
+      filtered_search.send_keys('author:')
+      filtered_search.send_keys(:backspace)
+      click_hint('author')
+
+      expect_tokens([{ name: 'author' }])
+      expect_filtered_search_input_empty
+    end
+
+    it 'reuses existing assignee text' do
+      filtered_search.send_keys('assignee:')
+      filtered_search.send_keys(:backspace)
+      click_hint('assignee')
+
+      expect_tokens([{ name: 'assignee' }])
+      expect_filtered_search_input_empty
+    end
+
+    it 'reuses existing milestone text' do
+      filtered_search.send_keys('milestone:')
+      filtered_search.send_keys(:backspace)
+      click_hint('milestone')
+
+      expect_tokens([{ name: 'milestone' }])
+      expect_filtered_search_input_empty
+    end
+
+    it 'reuses existing label text' do
+      filtered_search.send_keys('label:')
+      filtered_search.send_keys(:backspace)
+      click_hint('label')
+
+      expect_tokens([{ name: 'label' }])
+      expect_filtered_search_input_empty
     end
   end
 end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index ab3b868fd3a7315ee2f0227ac99a806180613534..b192064b693151fd0320795abacc2d77d8b8c560 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
 
       filtered_search.native.send_keys(:down, :down, :enter)
 
-      expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+      expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
     end
 
     it 'filters by case-insensitive name with or without symbol' do
-      search_for_label('b')
+      filtered_search.send_keys('b')
 
       expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
       expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
       clear_search_field
       init_label_search
 
-      search_for_label('~bu')
+      filtered_search.send_keys('~bu')
 
       expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
       expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
       click_label(bug_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+      expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name when the label is partially filled' do
@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
       click_label(bug_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+      expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name that contains multiple words' do
       click_label(two_words_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
+      expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name that contains multiple words and is very long' do
       click_label(long_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
+      expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name that contains double quotes' do
       click_label(wont_fix_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
+      expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name with the correct capitalization' do
       click_label(uppercase_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
+      expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the label name with special characters' do
       click_label(special_label.title)
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:~#{special_label.title} ")
+      expect_tokens([{ name: 'label', value: "~#{special_label.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'selects `no label`' do
       find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
 
       expect(page).not_to have_css(js_dropdown_label)
-      expect(filtered_search.value).to eq("label:none ")
+      expect_tokens([{ name: 'label', value: 'none' }])
+      expect_filtered_search_input_empty
     end
   end
 
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 0ce16715b8644940c308af73f3e13b4504f6a917..ce96a420699bdaaf7f9a90ac6216c3f4a347d142 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
-describe 'Dropdown milestone', js: true, feature: true do
-  include WaitForAjax
+describe 'Dropdown milestone', :feature, :js do
+  include FilteredSearchHelpers
 
   let!(:project) { create(:empty_project) }
   let!(:user) { create(:user) }
@@ -14,18 +14,10 @@ describe 'Dropdown milestone', js: true, feature: true do
 
   let(:filtered_search) { find('.filtered-search') }
   let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
-
-  def send_keys_to_filtered_search(input)
-    input.split("").each do |i|
-      filtered_search.send_keys(i)
-      sleep 3
-      wait_for_ajax
-      sleep 3
-    end
-  end
+  let(:filter_dropdown) { find("#{js_dropdown_milestone} .filter-dropdown") }
 
   def dropdown_milestone_size
-    page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
+    filter_dropdown.all('.filter-dropdown-item').size
   end
 
   def click_milestone(text)
@@ -64,13 +56,14 @@ describe 'Dropdown milestone', js: true, feature: true do
     end
 
     it 'should hide loading indicator when loaded' do
-      send_keys_to_filtered_search('milestone:')
+      filtered_search.set('milestone:')
 
-      expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
+      expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
+      expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
     end
 
     it 'should load all the milestones when opened' do
-      send_keys_to_filtered_search('milestone:')
+      filtered_search.set('milestone:')
 
       expect(dropdown_milestone_size).to be > 0
     end
@@ -78,41 +71,48 @@ describe 'Dropdown milestone', js: true, feature: true do
 
   describe 'filtering' do
     before do
-      filtered_search.set('milestone')
+      filtered_search.set('milestone:')
+
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
     end
 
     it 'filters by name' do
-      send_keys_to_filtered_search(':v1')
+      filtered_search.send_keys('v1')
 
       expect(dropdown_milestone_size).to eq(1)
     end
 
     it 'filters by case insensitive name' do
-      send_keys_to_filtered_search(':V1')
+      filtered_search.send_keys('V1')
 
       expect(dropdown_milestone_size).to eq(1)
     end
 
     it 'filters by name with symbol' do
-      send_keys_to_filtered_search(':%v1')
+      filtered_search.send_keys('%v1')
 
       expect(dropdown_milestone_size).to eq(1)
     end
 
     it 'filters by case insensitive name with symbol' do
-      send_keys_to_filtered_search(':%V1')
+      filtered_search.send_keys('%V1')
 
       expect(dropdown_milestone_size).to eq(1)
     end
 
     it 'filters by special characters' do
-      send_keys_to_filtered_search(':(+')
+      filtered_search.send_keys('(+')
 
       expect(dropdown_milestone_size).to eq(1)
     end
 
     it 'filters by special characters with symbol' do
-      send_keys_to_filtered_search(':%(+')
+      filtered_search.send_keys('%(+')
 
       expect(dropdown_milestone_size).to eq(1)
     end
@@ -121,70 +121,94 @@ describe 'Dropdown milestone', js: true, feature: true do
   describe 'selecting from dropdown' do
     before do
       filtered_search.set('milestone:')
+
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+      expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
     end
 
     it 'fills in the milestone name when the milestone has not been filled' do
       click_milestone(milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+      expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name when the milestone is partially filled' do
-      send_keys_to_filtered_search('v')
+      filtered_search.send_keys('v')
       click_milestone(milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+      expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name that contains multiple words' do
       click_milestone(two_words_milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
+      expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name that contains multiple words and is very long' do
       click_milestone(long_milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
+      expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name that contains double quotes' do
       click_milestone(wont_fix_milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
+      expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name with the correct capitalization' do
       click_milestone(uppercase_milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
+      expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'fills in the milestone name with special characters' do
       click_milestone(special_milestone.title)
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
+      expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'selects `no milestone`' do
       click_static_milestone('No Milestone')
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:none ")
+      expect_tokens([{ name: 'milestone', value: 'none' }])
+      expect_filtered_search_input_empty
     end
 
     it 'selects `upcoming milestone`' do
       click_static_milestone('Upcoming')
 
       expect(page).to have_css(js_dropdown_milestone, visible: false)
-      expect(filtered_search.value).to eq("milestone:upcoming ")
+      expect_tokens([{ name: 'milestone', value: 'upcoming' }])
+      expect_filtered_search_input_empty
+    end
+
+    it 'selects `started milestones`' do
+      click_static_milestone('Started')
+
+      expect(page).to have_css(js_dropdown_milestone, visible: false)
+      expect_tokens([{ name: 'milestone', value: 'started' }])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -222,16 +246,14 @@ describe 'Dropdown milestone', js: true, feature: true do
 
   describe 'caching requests' do
     it 'caches requests after the first load' do
-      filtered_search.set('milestone')
-      send_keys_to_filtered_search(':')
+      filtered_search.set('milestone:')
       initial_size = dropdown_milestone_size
 
       expect(initial_size).to be > 0
 
       create(:milestone, project: project)
       find('.filtered-search-input-container .clear-search').click
-      filtered_search.set('milestone')
-      send_keys_to_filtered_search(':')
+      filtered_search.set('milestone:')
 
       expect(dropdown_milestone_size).to eq(initial_size)
     end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 0420e64d42c4bf46460f77aa19761649fc60e648..f463312bf57c4e3b030d20cf99e7782e435089d1 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,4 +1,4 @@
-require 'rails_helper'
+require 'spec_helper'
 
 describe 'Filter issues', js: true, feature: true do
   include FilteredSearchHelpers
@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do
   let!(:project) { create(:project, group: group) }
   let!(:user) { create(:user) }
   let!(:user2) { create(:user) }
-  let!(:milestone) { create(:milestone, project: project) }
   let!(:label) { create(:label, project: project) }
   let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
 
   let!(:bug_label) { create(:label, project: project, title: 'bug') }
   let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
-  let!(:milestone) { create(:milestone, title: "8", project: project) }
+  let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
   let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
 
   let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
@@ -97,7 +96,9 @@ describe 'Filter issues', js: true, feature: true do
       it 'filters issues by searched author' do
         input_filtered_search("author:@#{user.username}")
 
+        expect_tokens([{ name: 'author', value: user.username }])
         expect_issues_list_count(5)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by invalid author' do
@@ -110,36 +111,50 @@ describe 'Filter issues', js: true, feature: true do
     end
 
     context 'author with other filters' do
+      let(:search_term) { 'issue' }
+
       it 'filters issues by searched author and text' do
-        search = "author:@#{user.username} issue"
-        input_filtered_search(search)
+        input_filtered_search("author:@#{user.username} #{search_term}")
 
+        expect_tokens([{ name: 'author', value: user.username }])
         expect_issues_list_count(3)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched author, assignee and text' do
-        search = "author:@#{user.username} assignee:@#{user.username} issue"
-        input_filtered_search(search)
+        input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username }
+        ])
         expect_issues_list_count(3)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched author, assignee, label, and text' do
-        search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue"
-        input_filtered_search(search)
+        input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username },
+          { name: 'label', value: caps_sensitive_label.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched author, assignee, label, milestone and text' do
-        search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue"
-        input_filtered_search(search)
+        input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username },
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'milestone', value: milestone.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
     end
 
@@ -151,19 +166,19 @@ describe 'Filter issues', js: true, feature: true do
   describe 'filter issues by assignee' do
     context 'only assignee' do
       it 'filters issues by searched assignee' do
-        search = "assignee:@#{user.username}"
-        input_filtered_search(search)
+        input_filtered_search("assignee:@#{user.username}")
 
+        expect_tokens([{ name: 'assignee', value: user.username }])
         expect_issues_list_count(5)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by no assignee' do
-        search = "assignee:none"
-        input_filtered_search(search)
+        input_filtered_search('assignee:none')
 
+        expect_tokens([{ name: 'assignee', value: 'none' }])
         expect_issues_list_count(8, 1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by invalid assignee' do
@@ -176,36 +191,50 @@ describe 'Filter issues', js: true, feature: true do
     end
 
     context 'assignee with other filters' do
+      let(:search_term) { 'searchTerm' }
+
       it 'filters issues by searched assignee and text' do
-        search = "assignee:@#{user.username} searchTerm"
-        input_filtered_search(search)
+        input_filtered_search("assignee:@#{user.username} #{search_term}")
 
+        expect_tokens([{ name: 'assignee', value: user.username }])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched assignee, author and text' do
-        search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
-        input_filtered_search(search)
+        input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'assignee', value: user.username },
+          { name: 'author', value: user.username }
+        ])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched assignee, author, label, text' do
-        search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm"
-        input_filtered_search(search)
+        input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'assignee', value: user.username },
+          { name: 'author', value: user.username },
+          { name: 'label', value: caps_sensitive_label.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched assignee, author, label, milestone and text' do
-        search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm"
-        input_filtered_search(search)
+        input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'assignee', value: user.username },
+          { name: 'author', value: user.username },
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'milestone', value: milestone.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
     end
 
@@ -217,21 +246,23 @@ describe 'Filter issues', js: true, feature: true do
   end
 
   describe 'filter issues by label' do
+    let(:search_term) { 'bug' }
+
     context 'only label' do
       it 'filters issues by searched label' do
-        search = "label:~#{bug_label.title}"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title}")
 
+        expect_tokens([{ name: 'label', value: bug_label.title }])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by no label' do
-        search = "label:none"
-        input_filtered_search(search)
+        input_filtered_search('label:none')
 
+        expect_tokens([{ name: 'label', value: 'none' }])
         expect_issues_list_count(9, 1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by invalid label' do
@@ -239,11 +270,14 @@ describe 'Filter issues', js: true, feature: true do
       end
 
       it 'filters issues by multiple labels' do
-        search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
 
+        expect_tokens([
+          { name: 'label', value: bug_label.title },
+          { name: 'label', value: caps_sensitive_label.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by label containing special characters' do
@@ -251,21 +285,20 @@ describe 'Filter issues', js: true, feature: true do
         special_issue = create(:issue, title: "Issue with special character label", project: project)
         special_issue.labels << special_label
 
-        search = "label:~#{special_label.title}"
-        input_filtered_search(search)
-
+        input_filtered_search("label:~#{special_label.title}")
+        expect_tokens([{ name: 'label', value: special_label.title }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'does not show issues' do
-        new_label = create(:label, project: project, title: "new_label")
+        new_label = create(:label, project: project, title: 'new_label')
 
-        search = "label:~#{new_label.title}"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{new_label.title}")
 
+        expect_tokens([{ name: 'label', value: new_label.title }])
         expect_no_issues_list()
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
     end
 
@@ -275,29 +308,29 @@ describe 'Filter issues', js: true, feature: true do
         special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
         special_multiple_issue.labels << special_multiple_label
 
-        search = "label:~'#{special_multiple_label.title}'"
-        input_filtered_search(search)
+        input_filtered_search("label:~'#{special_multiple_label.title}'")
 
+        # filtered search defaults quotations to double quotes
+        expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
         expect_issues_list_count(1)
 
-        # filtered search defaults quotations to double quotes
-        expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
+        expect_filtered_search_input_empty
       end
 
       it 'single quotes' do
-        search = "label:~'#{multiple_words_label.title}'"
-        input_filtered_search(search)
+        input_filtered_search("label:~'#{multiple_words_label.title}'")
 
+        expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
         expect_issues_list_count(1)
-        expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
+        expect_filtered_search_input_empty
       end
 
       it 'double quotes' do
-        search = "label:~\"#{multiple_words_label.title}\""
-        input_filtered_search(search)
+        input_filtered_search("label:~\"#{multiple_words_label.title}\"")
 
+        expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'single quotes containing double quotes' do
@@ -305,11 +338,11 @@ describe 'Filter issues', js: true, feature: true do
         double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
         double_quotes_label_issue.labels << double_quotes_label
 
-        search = "label:~'#{double_quotes_label.title}'"
-        input_filtered_search(search)
+        input_filtered_search("label:~'#{double_quotes_label.title}'")
 
+        expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'double quotes containing single quotes' do
@@ -317,86 +350,115 @@ describe 'Filter issues', js: true, feature: true do
         single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
         single_quotes_label_issue.labels << single_quotes_label
 
-        search = "label:~\"#{single_quotes_label.title}\""
-        input_filtered_search(search)
+        input_filtered_search("label:~\"#{single_quotes_label.title}\"")
 
+        expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
     end
 
     context 'label with other filters' do
       it 'filters issues by searched label and text' do
-        search = "label:~#{caps_sensitive_label.title} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
 
+        expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, author and text' do
-        search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, author, assignee and text' do
-        search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, author, assignee, milestone and text' do
-        search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username },
+          { name: 'milestone', value: milestone.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
     end
 
     context 'multiple labels with other filters' do
       it 'filters issues by searched label, label2, and text' do
-        search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: bug_label.title },
+          { name: 'label', value: caps_sensitive_label.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, label2, author and text' do
-        search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: bug_label.title },
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, label2, author, assignee and text' do
-        search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: bug_label.title },
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched label, label2, author, assignee, milestone and text' do
-        search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
-        input_filtered_search(search)
+        input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
 
+        expect_tokens([
+          { name: 'label', value: bug_label.title },
+          { name: 'label', value: caps_sensitive_label.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username },
+          { name: 'milestone', value: milestone.title }
+        ])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
     end
 
     context 'issue label clicked' do
       before do
         find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
-        sleep 1
       end
 
       it 'filters' do
@@ -404,7 +466,8 @@ describe 'Filter issues', js: true, feature: true do
       end
 
       it 'displays in search bar' do
-        expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"")
+        expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
+        expect_filtered_search_input_empty
       end
     end
 
@@ -420,19 +483,33 @@ describe 'Filter issues', js: true, feature: true do
       it 'filters issues by searched milestone' do
         input_filtered_search("milestone:%#{milestone.title}")
 
+        expect_tokens([{ name: 'milestone', value: milestone.title }])
         expect_issues_list_count(5)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by no milestone' do
         input_filtered_search("milestone:none")
 
+        expect_tokens([{ name: 'milestone', value: 'none' }])
         expect_issues_list_count(7, 1)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by upcoming milestones' do
         input_filtered_search("milestone:upcoming")
 
+        expect_tokens([{ name: 'milestone', value: 'upcoming' }])
         expect_issues_list_count(1)
+        expect_filtered_search_input_empty
+      end
+
+      it 'filters issues by started milestones' do
+        input_filtered_search("milestone:started")
+
+        expect_tokens([{ name: 'milestone', value: 'started' }])
+        expect_issues_list_count(5)
+        expect_filtered_search_input_empty
       end
 
       it 'filters issues by invalid milestones' do
@@ -447,55 +524,69 @@ describe 'Filter issues', js: true, feature: true do
         special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
         create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
 
-        search = "milestone:%#{special_milestone.title}"
-        input_filtered_search(search)
+        input_filtered_search("milestone:%#{special_milestone.title}")
 
+        expect_tokens([{ name: 'milestone', value: special_milestone.title }])
         expect_issues_list_count(1)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
 
       it 'does not show issues' do
         new_milestone = create(:milestone, title: "new", project: project)
 
-        search = "milestone:%#{new_milestone.title}"
-        input_filtered_search(search)
+        input_filtered_search("milestone:%#{new_milestone.title}")
 
+        expect_tokens([{ name: 'milestone', value: new_milestone.title }])
         expect_no_issues_list()
-        expect_filtered_search_input(search)
+        expect_filtered_search_input_empty
       end
     end
 
     context 'milestone with other filters' do
+      let(:search_term) { 'bug' }
+
       it 'filters issues by searched milestone and text' do
-        search = "milestone:%#{milestone.title} bug"
-        input_filtered_search(search)
+        input_filtered_search("milestone:%#{milestone.title} #{search_term}")
 
+        expect_tokens([{ name: 'milestone', value: milestone.title }])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched milestone, author and text' do
-        search = "milestone:%#{milestone.title} author:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'milestone', value: milestone.title },
+          { name: 'author', value: user.username }
+        ])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched milestone, author, assignee and text' do
-        search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug"
-        input_filtered_search(search)
+        input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
 
+        expect_tokens([
+          { name: 'milestone', value: milestone.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username }
+        ])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
 
       it 'filters issues by searched milestone, author, assignee, label and text' do
-        search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug"
-        input_filtered_search(search)
-
+        input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
+
+        expect_tokens([
+          { name: 'milestone', value: milestone.title },
+          { name: 'author', value: user.username },
+          { name: 'assignee', value: user.username },
+          { name: 'label', value: bug_label.title }
+        ])
         expect_issues_list_count(2)
-        expect_filtered_search_input(search)
+        expect_filtered_search_input(search_term)
       end
     end
 
@@ -506,44 +597,6 @@ describe 'Filter issues', js: true, feature: true do
     end
   end
 
-  describe 'overwrites selected filter' do
-    it 'changes author' do
-      input_filtered_search("author:@#{user.username}", submit: false)
-
-      select_search_at_index(3)
-
-      page.within '#js-dropdown-author' do
-        click_button user2.username
-      end
-
-      expect(filtered_search.value).to eq("author:@#{user2.username} ")
-    end
-
-    it 'changes label' do
-      input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
-
-      select_search_at_index(27)
-
-      page.within '#js-dropdown-label' do
-        click_button label.name
-      end
-
-      expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
-    end
-
-    it 'changes label correctly space is in previous label' do
-      input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
-
-      select_search_at_index(0)
-
-      page.within '#js-dropdown-label' do
-        click_button label.name
-      end
-
-      expect(filtered_search.value).to eq("label:~#{label.name} ")
-    end
-  end
-
   describe 'filter issues by text' do
     context 'only text' do
       it 'filters issues by searched text' do
@@ -605,80 +658,81 @@ describe 'Filter issues', js: true, feature: true do
 
     context 'searched text with other filters' do
       it 'filters issues by searched text and author' do
+        # After searching, all search terms are placed at the end
         input_filtered_search("bug author:@#{user.username}")
 
         expect_issues_list_count(2)
-        expect_filtered_search_input("author:@#{user.username} bug")
+        expect_filtered_search_input('bug')
       end
 
       it 'filters issues by searched text, author and more text' do
         input_filtered_search("bug author:@#{user.username} report")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} bug report")
+        expect_filtered_search_input('bug report')
       end
 
       it 'filters issues by searched text, author and assignee' do
         input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
 
         expect_issues_list_count(2)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
+        expect_filtered_search_input('bug')
       end
 
       it 'filters issues by searched text, author, more text and assignee' do
         input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
+        expect_filtered_search_input('bug report')
       end
 
       it 'filters issues by searched text, author, more text, assignee and even more text' do
         input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with")
+        expect_filtered_search_input('bug report with')
       end
 
       it 'filters issues by searched text, author, assignee and label' do
         input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
 
         expect_issues_list_count(2)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug")
+        expect_filtered_search_input('bug')
       end
 
       it 'filters issues by searched text, author, text, assignee, text, label and text' do
         input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything")
+        expect_filtered_search_input('bug report with everything')
       end
 
       it 'filters issues by searched text, author, assignee, label and milestone' do
         input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
 
         expect_issues_list_count(2)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug")
+        expect_filtered_search_input('bug')
       end
 
       it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
         input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you")
+        expect_filtered_search_input('bug report with everything you')
       end
 
       it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
         input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug")
+        expect_filtered_search_input('bug')
       end
 
       it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
         input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
 
         expect_issues_list_count(1)
-        expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought")
+        expect_filtered_search_input('bug report with everything you thought')
       end
     end
 
@@ -717,8 +771,8 @@ describe 'Filter issues', js: true, feature: true do
     before do
       input_filtered_search('bug')
 
-      # Wait for search results to load
-      sleep 2
+      # This ensures that the search is performed
+      expect_issues_list_count(4, 1)
     end
 
     it 'open state' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 90eb60eb3376325a862711b25c2c802be35905d5..59244d65eecc94a005ef798c9fc0dc9561d633a3 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,7 @@
 require 'rails_helper'
 
 describe 'Search bar', js: true, feature: true do
+  include FilteredSearchHelpers
   include WaitForAjax
 
   let!(:project) { create(:empty_project) }
@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
     it 'selects item' do
       filtered_search.native.send_keys(:down, :down, :enter)
 
-      expect(filtered_search.value).to eq('author:')
+      expect_tokens([{ name: 'author' }])
+      expect_filtered_search_input_empty
     end
   end
 
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96e87c82d2c1c1e799b70daa040b3f119b58e3b5
--- /dev/null
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -0,0 +1,352 @@
+require 'rails_helper'
+
+describe 'Visual tokens', js: true, feature: true do
+  include FilteredSearchHelpers
+
+  let!(:project) { create(:empty_project) }
+  let!(:user) { create(:user, name: 'administrator', username: 'root') }
+  let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
+  let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
+  let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
+  let!(:label) { create(:label, project: project, title: 'abc') }
+  let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') }
+
+  let(:filtered_search) { find('.filtered-search') }
+  let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
+  let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
+  let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
+  let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+  def is_input_focused
+    page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
+  end
+
+  before do
+    project.add_user(user, :master)
+    project.add_user(user_rock, :master)
+    login_as(user)
+    create(:issue, project: project)
+
+    visit namespace_project_issues_path(project.namespace, project)
+  end
+
+  describe 'editing author token' do
+    before do
+      input_filtered_search('author:@root assignee:none', submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+    end
+
+    it 'opens author dropdown' do
+      expect(page).to have_css('#js-dropdown-author', visible: true)
+    end
+
+    it 'makes value editable' do
+      expect_filtered_search_input('@root')
+    end
+
+    it 'filters value' do
+      filtered_search.send_keys(:backspace)
+
+      expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1)
+    end
+
+    it 'ends editing mode when document is clicked' do
+      find('#content-body').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-author', visible: false)
+    end
+
+    it 'ends editing mode when scroll container is clicked' do
+      find('.scroll-container').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-author', visible: false)
+    end
+
+    describe 'selecting different author from dropdown' do
+      before do
+        filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
+      end
+
+      it 'changes value in visual token' do
+        expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+      end
+
+      it 'moves input to the right' do
+        expect(is_input_focused).to eq(true)
+      end
+    end
+  end
+
+  describe 'editing assignee token' do
+    before do
+      input_filtered_search('assignee:@root author:none', submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+    end
+
+    it 'opens assignee dropdown' do
+      expect(page).to have_css('#js-dropdown-assignee', visible: true)
+    end
+
+    it 'makes value editable' do
+      expect_filtered_search_input('@root')
+    end
+
+    it 'filters value' do
+      filtered_search.send_keys(:backspace)
+
+      expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
+    end
+
+    it 'ends editing mode when document is clicked' do
+      find('#content-body').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-assignee', visible: false)
+    end
+
+    it 'ends editing mode when scroll container is clicked' do
+      find('.scroll-container').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-assignee', visible: false)
+    end
+
+    describe 'selecting static option from dropdown' do
+      before do
+        find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+      end
+
+      it 'changes value in visual token' do
+        expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
+      end
+
+      it 'moves input to the right' do
+        expect(is_input_focused).to eq(true)
+      end
+    end
+  end
+
+  describe 'editing milestone token' do
+    before do
+      input_filtered_search('milestone:%10.0 author:none', submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+      first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
+    end
+
+    it 'opens milestone dropdown' do
+      expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
+      expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
+      expect(page).to have_css('#js-dropdown-milestone', visible: true)
+    end
+
+    it 'selects static option from dropdown' do
+      find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
+
+      expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
+      expect(is_input_focused).to eq(true)
+    end
+
+    it 'makes value editable' do
+      expect_filtered_search_input('%10.0')
+    end
+
+    it 'filters value' do
+      filtered_search.send_keys(:backspace)
+
+      expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
+    end
+
+    it 'ends editing mode when document is clicked' do
+      find('#content-body').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-milestone', visible: false)
+    end
+
+    it 'ends editing mode when scroll container is clicked' do
+      find('.scroll-container').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-milestone', visible: false)
+    end
+  end
+
+  describe 'editing label token' do
+    before do
+      input_filtered_search("label:~#{label.title} author:none", submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+      first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
+    end
+
+    it 'opens label dropdown' do
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+      expect(page).to have_css('#js-dropdown-label', visible: true)
+    end
+
+    it 'selects option from dropdown' do
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+      find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
+
+      expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
+      expect(is_input_focused).to eq(true)
+    end
+
+    it 'makes value editable' do
+      expect_filtered_search_input("~#{label.title}")
+    end
+
+    it 'filters value' do
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+      expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+      filtered_search.send_keys(:backspace)
+
+      filter_label_dropdown.find('.filter-dropdown-item')
+
+      expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
+    end
+
+    it 'ends editing mode when document is clicked' do
+      find('#content-body').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-label', visible: false)
+    end
+
+    it 'ends editing mode when scroll container is clicked' do
+      find('.scroll-container').click
+
+      expect_filtered_search_input_empty
+      expect(page).to have_css('#js-dropdown-label', visible: false)
+    end
+  end
+
+  describe 'editing multiple tokens' do
+    before do
+      input_filtered_search('author:@root assignee:none', submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+    end
+
+    it 'opens author dropdown' do
+      expect(page).to have_css('#js-dropdown-author', visible: true)
+    end
+
+    it 'opens assignee dropdown' do
+      find('.tokens-container .filtered-search-token', text: 'Assignee').double_click
+      expect(page).to have_css('#js-dropdown-assignee', visible: true)
+    end
+  end
+
+  describe 'editing a search term while editing another filter token' do
+    before do
+      input_filtered_search('author assignee:', submit: false)
+      first('.tokens-container .filtered-search-term').double_click
+    end
+
+    it 'opens hint dropdown' do
+      expect(page).to have_css('#js-dropdown-hint', visible: true)
+    end
+
+    it 'opens author dropdown' do
+      find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click
+
+      expect(page).to have_css('#js-dropdown-author', visible: true)
+    end
+  end
+
+  describe 'add new token after editing existing token' do
+    before do
+      input_filtered_search('author:@root assignee:none', submit: false)
+      first('.tokens-container .filtered-search-token').double_click
+      filtered_search.send_keys(' ')
+    end
+
+    describe 'opens dropdowns' do
+      it 'opens hint dropdown' do
+        expect(page).to have_css('#js-dropdown-hint', visible: true)
+      end
+
+      it 'opens author dropdown' do
+        filtered_search.send_keys('author:')
+        expect(page).to have_css('#js-dropdown-author', visible: true)
+      end
+
+      it 'opens assignee dropdown' do
+        filtered_search.send_keys('assignee:')
+        expect(page).to have_css('#js-dropdown-assignee', visible: true)
+      end
+
+      it 'opens milestone dropdown' do
+        filtered_search.send_keys('milestone:')
+        expect(page).to have_css('#js-dropdown-milestone', visible: true)
+      end
+
+      it 'opens label dropdown' do
+        filtered_search.send_keys('label:')
+        expect(page).to have_css('#js-dropdown-label', visible: true)
+      end
+    end
+
+    describe 'creates visual tokens' do
+      it 'creates author token' do
+        filtered_search.send_keys('author:@thomas ')
+        token = page.all('.tokens-container .filtered-search-token')[1]
+
+        expect(token.find('.name').text).to eq('Author')
+        expect(token.find('.value').text).to eq('@thomas')
+      end
+
+      it 'creates assignee token' do
+        filtered_search.send_keys('assignee:@thomas ')
+        token = page.all('.tokens-container .filtered-search-token')[1]
+
+        expect(token.find('.name').text).to eq('Assignee')
+        expect(token.find('.value').text).to eq('@thomas')
+      end
+
+      it 'creates milestone token' do
+        filtered_search.send_keys('milestone:none ')
+        token = page.all('.tokens-container .filtered-search-token')[1]
+
+        expect(token.find('.name').text).to eq('Milestone')
+        expect(token.find('.value').text).to eq('none')
+      end
+
+      it 'creates label token' do
+        filtered_search.send_keys('label:~Backend ')
+        token = page.all('.tokens-container .filtered-search-token')[1]
+
+        expect(token.find('.name').text).to eq('Label')
+        expect(token.find('.value').text).to eq('~Backend')
+      end
+    end
+
+    it 'does not tokenize incomplete token' do
+      filtered_search.send_keys('author:')
+
+      find('#content-body').click
+      token = page.all('.tokens-container .js-visual-token')[1]
+
+      expect_filtered_search_input_empty
+      expect(token.find('.name').text).to eq('Author')
+    end
+  end
+
+  describe 'search using incomplete visual tokens' do
+    before do
+      input_filtered_search('author:@root assignee:none', extra_space: false)
+    end
+
+    it 'tokenizes the search term to complete visual token' do
+      expect_tokens([
+        { name: 'author', value: '@root' },
+        { name: 'assignee', value: 'none' }
+      ])
+    end
+  end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 741ca95f1ca0a770d6ce468ca268f1979dea9d12..755992069ffe557efe370a4987eea3d7211364b7 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -1,8 +1,11 @@
 require 'rails_helper'
 
 describe 'New/edit issue', feature: true, js: true do
+  include GitlabRoutingHelper
+
   let!(:project)   { create(:project) }
   let!(:user)      { create(:user)}
+  let!(:user2)     { create(:user)}
   let!(:milestone) { create(:milestone, project: project) }
   let!(:label)     { create(:label, project: project) }
   let!(:label2)    { create(:label, project: project) }
@@ -10,6 +13,7 @@ describe 'New/edit issue', feature: true, js: true do
 
   before do
     project.team << [user, :master]
+    project.team << [user2, :master]
     login_as(user)
   end
 
@@ -22,14 +26,23 @@ describe 'New/edit issue', feature: true, js: true do
       fill_in 'issue_title', with: 'title'
       fill_in 'issue_description', with: 'title'
 
+      expect(find('a', text: 'Assign to me')).to be_visible
       click_button 'Assignee'
       page.within '.dropdown-menu-user' do
-        click_link user.name
+        click_link user2.name
+      end
+      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+      page.within '.js-assignee-search' do
+        expect(page).to have_content user2.name
       end
+      expect(find('a', text: 'Assign to me')).to be_visible
+
+      click_link 'Assign to me'
       expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
       page.within '.js-assignee-search' do
         expect(page).to have_content user.name
       end
+      expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
 
       click_button 'Milestone'
       page.within '.issue-milestone' do
@@ -67,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do
           expect(page).to have_content label2.title
         end
       end
+
+      page.within '.issuable-meta' do
+        issue = Issue.find_by(title: 'title')
+
+        expect(page).to have_text("Issue #{issue.to_reference}")
+        # compare paths because the host differ in test
+        expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
+      end
     end
 
     it 'correctly updates the dropdown toggle when removing a label' do
@@ -94,6 +115,7 @@ describe 'New/edit issue', feature: true, js: true do
     it 'allows user to update issue' do
       expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
       expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+      expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
 
       page.within '.js-user-search' do
         expect(page).to have_content user.name
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 094f645a077cf510b5f3b0b572308750a4f36f79..a58aedc924e40805f1794975b3a1f6b9cccb7e53 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,11 +1,12 @@
 require 'spec_helper'
 
 describe 'Issues', feature: true do
+  include DropzoneHelper
   include IssueHelpers
   include SortingHelper
   include WaitForAjax
 
-  let(:project) { create(:project) }
+  let(:project) { create(:project, :public) }
 
   before do
     login_as :user
@@ -150,7 +151,7 @@ describe 'Issues', feature: true do
 
   describe 'Filter issue' do
     before do
-      ['foobar', 'barbaz', 'gitlab'].each do |title|
+      %w(foobar barbaz gitlab).each do |title|
         create(:issue,
                author: @user,
                assignee: @user,
@@ -564,19 +565,40 @@ describe 'Issues', feature: true do
   end
 
   describe 'new issue' do
+    context 'by unauthenticated user' do
+      before do
+        logout
+      end
+
+      it 'redirects to signin then back to new issue after signin' do
+        visit namespace_project_issues_path(project.namespace, project)
+
+        click_link 'New issue'
+
+        expect(current_path).to eq new_user_session_path
+
+        login_as :user
+
+        expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project)
+      end
+    end
+
     context 'dropzone upload file', js: true do
       before do
         visit new_namespace_project_issue_path(project.namespace, project)
       end
 
       it 'uploads file when dragging into textarea' do
-        drop_in_dropzone test_image_file
-
-        # Wait for the file to upload
-        sleep 1
+        dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
 
         expect(page.find_field("issue_description").value).to have_content 'banana_sample'
       end
+
+      it 'adds double newline to end of attachment markdown' do
+        dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+
+        expect(page.find_field("issue_description").value).to match /\n\n$/
+      end
     end
   end
 
@@ -656,25 +678,4 @@ describe 'Issues', feature: true do
       end
     end
   end
-
-  def drop_in_dropzone(file_path)
-    # Generate a fake input selector
-    page.execute_script <<-JS
-      var fakeFileInput = window.$('<input/>').attr(
-        {id: 'fakeFileInput', type: 'file'}
-      ).appendTo('body');
-    JS
-    # Attach the file to the fake input selector with Capybara
-    attach_file("fakeFileInput", file_path)
-    # Add the file to a fileList array and trigger the fake drop event
-    page.execute_script <<-JS
-      var fileList = [$('#fakeFileInput')[0].files[0]];
-      var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
-      $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
-    JS
-  end
-
-  def test_image_file
-    File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
-  end
 end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index ae609160e18ca8b0bad7cf3b4a838b13f29e600e..f32d1f78b403e96fec028a4e488b6d1970bae15d 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -48,6 +48,18 @@ feature 'Login', feature: true do
     end
   end
 
+  describe 'with the ghost user' do
+    it 'disallows login' do
+      login_with(User.ghost)
+
+      expect(page).to have_content('Invalid Login or password.')
+    end
+
+    it 'does not update Devise trackable attributes' do
+      expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count }
+    end
+  end
+
   describe 'with two-factor authentication' do
     def enter_code(code)
       fill_in 'user_otp_attempt', with: code
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 32159559c379bcd530929728d6082f007b5d53de..894df13a2dcc8b92688f9079da454b2e8c3fedde 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -115,6 +115,14 @@ describe 'GitLab Markdown', feature: true do
         expect(doc).to have_selector('span:contains("span tag")')
       end
 
+      it 'permits details elements' do
+        expect(doc).to have_selector('details:contains("Hiding the details")')
+      end
+
+      it 'permits summary elements' do
+        expect(doc).to have_selector('details summary:contains("collapsible")')
+      end
+
       it 'permits style attribute in th elements' do
         aggregate_failures do
           expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index d710a780111408b0e412f76d70f7118beb8aef49..18508a44184373de30205b3a171770b51fe8ea34 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -154,7 +154,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
     'conflict-binary-file' => 'when the conflicts contain a binary file',
     'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
     'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
-  }
+  }.freeze
 
   UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
     context description do
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 73c5ef31edc5513d65d40522459efcc258259918..18833ba72668d30a155f27a50ea4f908d338ab76 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
         expect(page).to have_content pipeline.status
         expect(page).to have_content pipeline.id
       end
-
-      expect(page.find('a.btn-remove')[:href])
-        .to include fork_project.path_with_namespace
     end
   end
 
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a6c72b0b3aca9761b7ee12295968f04c8a7f0382
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -0,0 +1,186 @@
+require 'spec_helper'
+
+feature 'Diff note avatars', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user)          { create(:user) }
+  let(:project)       { create(:project, :public) }
+  let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+  let(:path)          { "files/ruby/popen.rb" }
+  let(:position) do
+    Gitlab::Diff::Position.new(
+      old_path: path,
+      new_path: path,
+      old_line: nil,
+      new_line: 9,
+      diff_refs: merge_request.diff_refs
+    )
+  end
+  let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+  end
+
+  context 'discussion tab' do
+    before do
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'does not show avatars on discussion tab' do
+      expect(page).not_to have_selector('.js-avatar-container')
+      expect(page).not_to have_selector('.diff-comment-avatar-holders')
+    end
+
+    it 'does not render avatars after commening on discussion tab' do
+      click_button 'Reply...'
+
+      page.within('.js-discussion-note-form') do
+        find('.note-textarea').native.send_keys('Test comment')
+
+        click_button 'Comment'
+      end
+
+      expect(page).to have_content('Test comment')
+      expect(page).not_to have_selector('.js-avatar-container')
+      expect(page).not_to have_selector('.diff-comment-avatar-holders')
+    end
+  end
+
+  context 'commit view' do
+    before do
+      visit namespace_project_commit_path(project.namespace, project, merge_request.commits.first.id)
+    end
+
+    it 'does not render avatar after commenting' do
+      first('.diff-line-num').trigger('mouseover')
+      find('.js-add-diff-note-button').click
+
+      page.within('.js-discussion-note-form') do
+        find('.note-textarea').native.send_keys('test comment')
+
+        click_button 'Comment'
+
+        wait_for_ajax
+      end
+
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+      expect(page).to have_content('test comment')
+      expect(page).not_to have_selector('.js-avatar-container')
+      expect(page).not_to have_selector('.diff-comment-avatar-holders')
+    end
+  end
+
+  %w(inline parallel).each do |view|
+    context "#{view} view" do
+      before do
+        visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+        wait_for_ajax
+      end
+
+      it 'shows note avatar' do
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          find('.diff-notes-collapse').click
+
+          expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+        end
+      end
+
+      it 'shows comment on note avatar' do
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          find('.diff-notes-collapse').click
+
+          expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+        end
+      end
+
+      it 'toggles comments when clicking avatar' do
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          find('.diff-notes-collapse').click
+        end
+
+        expect(page).to have_selector('.notes_holder', visible: false)
+
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          first('img.js-diff-comment-avatar').click
+        end
+
+        expect(page).to have_selector('.notes_holder')
+      end
+
+      it 'removes avatar when note is deleted' do
+        page.within find(".note-row-#{note.id}") do
+          find('.js-note-delete').click
+        end
+
+        wait_for_ajax
+
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          expect(page).not_to have_selector('img.js-diff-comment-avatar')
+        end
+      end
+
+      it 'adds avatar when commenting' do
+        click_button 'Reply...'
+
+        page.within '.js-discussion-note-form' do
+          find('.js-note-text').native.send_keys('Test')
+
+          click_button 'Comment'
+
+          wait_for_ajax
+        end
+
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          find('.diff-notes-collapse').click
+
+          expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+        end
+      end
+
+      it 'adds multiple comments' do
+        3.times do
+          click_button 'Reply...'
+
+          page.within '.js-discussion-note-form' do
+            find('.js-note-text').native.send_keys('Test')
+
+            find('.js-comment-button').trigger 'click'
+
+            wait_for_ajax
+          end
+        end
+
+        page.within find("[id='#{position.line_code(project.repository)}']") do
+          find('.diff-notes-collapse').click
+
+          expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+          expect(find('.diff-comments-more-count')).to have_content '+1'
+        end
+      end
+
+      context 'multiple comments' do
+        before do
+          create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+          create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+          create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+          visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+          wait_for_ajax
+        end
+
+        it 'shows extra comment count' do
+          page.within find("[id='#{position.line_code(project.repository)}']") do
+            find('.diff-notes-collapse').click
+
+            expect(find('.diff-comments-more-count')).to have_content '+1'
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 5608cda28f8601b4cae87d01fcd817ac4f1db882..265a0cfc1980fac61373c4a200622578eafae53c 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
     visit_merge_requests(project)
     input_filtered_search('milestone:none')
 
+    expect_tokens([{ name: 'milestone', value: 'none' }])
+    expect_filtered_search_input_empty
+
     expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
     expect(page).to have_css('.merge-request', count: 1)
   end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 6579a88d4ab15266d2578839aed57345ef67c3d4..70e3997e716d3f26754e4477108d9574da93bc95 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
   describe 'for assignee from mr#index' do
     let(:search_query) { "assignee:@#{user.username}" }
 
+    def expect_assignee_visual_tokens
+      expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
+    end
+
     before do
       input_filtered_search(search_query)
 
@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
 
     context 'assignee', js: true do
       it 'updates to current user' do
-        expect_filtered_search_input(search_query)
+        expect_assignee_visual_tokens()
       end
 
       it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
-        expect_filtered_search_input(search_query)
+        expect_assignee_visual_tokens()
       end
 
       it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
-        expect_filtered_search_input(search_query)
+        expect_assignee_visual_tokens()
       end
     end
   end
 
   describe 'for milestone from mr#index' do
-    let(:search_query) { "milestone:%#{milestone.title}" }
+    let(:search_query) { "milestone:%\"#{milestone.title}\"" }
+
+    def expect_milestone_visual_tokens
+      expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
+      expect_filtered_search_input_empty
+    end
 
     before do
       input_filtered_search(search_query)
@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
 
     context 'milestone', js: true do
       it 'updates to current milestone' do
-        expect_filtered_search_input(search_query)
+        expect_milestone_visual_tokens()
       end
 
       it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
-        expect_filtered_search_input(search_query)
+        expect_milestone_visual_tokens()
       end
 
       it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
-        expect_filtered_search_input(search_query)
+        expect_milestone_visual_tokens()
       end
     end
   end
@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
       input_filtered_search('label:none')
 
       expect_mr_list_count(1)
-      expect_filtered_search_input('label:none')
+      expect_tokens([{ name: 'label', value: 'none' }])
+      expect_filtered_search_input_empty
     end
 
     it 'filters by a label' do
       input_filtered_search("label:~#{label.title}")
 
       expect_mr_list_count(0)
-      expect_filtered_search_input("label:~#{label.title}")
+      expect_tokens([{ name: 'label', value: "~#{label.title}" }])
+      expect_filtered_search_input_empty
     end
 
     it "filters by `won't fix` and another label" do
       input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
 
       expect_mr_list_count(0)
-      expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+      expect_tokens([
+        { name: 'label', value: "~\"#{wontfix.title}\"" },
+        { name: 'label', value: "~#{label.title}" }
+      ])
+      expect_filtered_search_input_empty
     end
 
     it "filters by `won't fix` label followed by another label after page load" do
       input_filtered_search("label:~\"#{wontfix.title}\"")
 
       expect_mr_list_count(0)
-      expect_filtered_search_input("label:~\"#{wontfix.title}\"")
-
-      input_filtered_search_keys(" label:~#{label.title}")
+      expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
+      expect_filtered_search_input_empty
 
-      expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+      input_filtered_search_keys("label:~#{label.title}")
 
       expect_mr_list_count(0)
-      expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+      expect_tokens([
+        { name: 'label', value: "~\"#{wontfix.title}\"" },
+        { name: 'label', value: "~#{label.title}" }
+      ])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
       input_filtered_search("assignee:@#{user.username}")
 
       expect_mr_list_count(1)
-      expect_filtered_search_input("assignee:@#{user.username}")
+      expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
 
-      input_filtered_search_keys(" label:~#{label.title}")
+      input_filtered_search_keys("label:~#{label.title} ")
 
       expect_mr_list_count(1)
 
@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
     end
 
     context 'assignee and label', js: true do
+      def expect_assignee_label_visual_tokens
+        expect_tokens([
+          { name: 'assignee', value: "@#{user.username}" },
+          { name: 'label', value: "~#{label.title}" }
+        ])
+        expect_filtered_search_input_empty
+      end
+
       it 'updates to current assignee and label' do
-        expect_filtered_search_input(search_query)
+        expect_assignee_label_visual_tokens()
       end
 
       it 'does not change when closed link is clicked' do
         find('.issues-state-filters a', text: "Closed").click
 
-        expect_filtered_search_input(search_query)
+        expect_assignee_label_visual_tokens()
       end
 
       it 'does not change when all link is clicked' do
         find('.issues-state-filters a', text: "All").click
 
-        expect_filtered_search_input(search_query)
+        expect_assignee_label_visual_tokens()
       end
     end
   end
@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
         input_filtered_search_keys(' label:~bug')
 
         expect_mr_list_count(1)
+        expect_tokens([{ name: 'label', value: '~bug' }])
+        expect_filtered_search_input('Bug')
       end
 
       it 'filters by text and milestone' do
@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
         input_filtered_search_keys(' milestone:%8')
 
         expect_mr_list_count(1)
+        expect_tokens([{ name: 'milestone', value: '%8' }])
+        expect_filtered_search_input('Bug')
       end
 
       it 'filters by text and assignee' do
@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
         input_filtered_search_keys(" assignee:@#{user.username}")
 
         expect_mr_list_count(1)
+        expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+        expect_filtered_search_input('Bug')
       end
 
       it 'filters by text and author' do
@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
         input_filtered_search_keys(" author:@#{user.username}")
 
         expect_mr_list_count(1)
+        expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+        expect_filtered_search_input('Bug')
       end
     end
   end
@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
     it 'filter by current user' do
       visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
 
-      expect_filtered_search_input("assignee:@#{user.username}")
+      expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'filter by new user' do
@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
 
       visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
 
-      expect_filtered_search_input("assignee:@#{new_user.username}")
+      expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
+      expect_filtered_search_input_empty
     end
   end
 
@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
     it 'filter by current user' do
       visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
 
-      expect_filtered_search_input("author:@#{user.username}")
+      expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+      expect_filtered_search_input_empty
     end
 
     it 'filter by new user' do
@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
 
       visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
 
-      expect_filtered_search_input("author:@#{new_user.username}")
+      expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
+      expect_filtered_search_input_empty
     end
   end
 end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 7594cbf54e857882625db0903bc1b3649d990507..f8518f450dc82d3e3c64aade35c808d80f48b560 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -1,15 +1,19 @@
 require 'rails_helper'
 
 describe 'New/edit merge request', feature: true, js: true do
+  include GitlabRoutingHelper
+
   let!(:project)   { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
   let(:fork_project) { create(:project, forked_from_project: project) }
   let!(:user)      { create(:user)}
+  let!(:user2)      { create(:user)}
   let!(:milestone) { create(:milestone, project: project) }
   let!(:label)     { create(:label, project: project) }
   let!(:label2)    { create(:label, project: project) }
 
   before do
     project.team << [user, :master]
+    project.team << [user2, :master]
   end
 
   context 'owned projects' do
@@ -33,8 +37,14 @@ describe 'New/edit merge request', feature: true, js: true do
       it 'creates new merge request' do
         click_button 'Assignee'
         page.within '.dropdown-menu-user' do
-          click_link user.name
+          click_link user2.name
+        end
+        expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+        page.within '.js-assignee-search' do
+          expect(page).to have_content user2.name
         end
+
+        click_link 'Assign to me'
         expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
         page.within '.js-assignee-search' do
           expect(page).to have_content user.name
@@ -76,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do
             expect(page).to have_content label2.title
           end
         end
+
+        page.within '.issuable-meta' do
+          merge_request = MergeRequest.find_by(source_branch: 'fix')
+
+          expect(page).to have_text("Merge Request #{merge_request.to_reference}")
+          # compare paths because the host differ in test
+          expect(find_link(merge_request.to_reference)[:href])
+            .to end_with(merge_request_path(merge_request))
+        end
       end
     end
 
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index f2f8f11ab283272d8c0d8abc18aaa5013a4da168..0ceaf7bc830bb1faf907a5fa5e6ca2917dea040a 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -34,7 +34,7 @@ feature 'Merge immediately', :feature, :js do
 
         click_link 'Merge Immediately'
 
-        expect(find('.js-merge-button')).to have_content('Merge in progress')
+        expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress')
       end
     end
   end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index 2ea9c317bd1a7737f35f8fa95177d7a072b89955..ed7193b97776613839d6864cf9114eaebb53d243 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -75,7 +75,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
       context 'when it was enabled and then canceled' do
         let(:merge_request) do
           create(:merge_request_with_diffs,
-                 :merge_when_build_succeeds,
+                 :merge_when_pipeline_succeeds,
                    source_project: project,
                    title: 'Bug NS-04',
                    author: user,
@@ -97,7 +97,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
                                                   author: user,
                                                   merge_user: user,
                                                   title: 'MepMep',
-                                                  merge_when_build_succeeds: true)
+                                                  merge_when_pipeline_succeeds: true)
     end
 
     let!(:build) do
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index d2f5c4afc93f8e3ddc274288e3638d018ccf8ee1..447764566e04d1df1f9878e16ea764911e458fff 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do
   let(:merge_request) { create(:merge_request_with_diffs) }
   let(:project)       { merge_request.target_project }
 
@@ -27,9 +27,9 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
       status: status)
     end
 
-    context 'when merge requests can only be merged if the build succeeds' do
+    context 'when merge requests can only be merged if the pipeline succeeds' do
       before do
-        project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+        project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
       end
 
       context 'when CI is running' do
@@ -88,7 +88,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
 
     context 'when merge requests can be merged when the build failed' do
       before do
-        project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+        project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
       end
 
       context 'when CI is running' do
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 58f11499e3fd47bb1baa688e57b2dae132e14106..14511707af4c6223d94530917666244587dc6dca 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-feature 'Issues filter reset button', feature: true, js: true do
+feature 'Merge requests filter clear button', feature: true, js: true do
   include FilteredSearchHelpers
   include MergeRequestHelpers
   include WaitForAjax
@@ -24,67 +24,113 @@ feature 'Issues filter reset button', feature: true, js: true do
   context 'when a milestone filter has been applied' do
     it 'resets the milestone filter' do
       visit_merge_requests(project, milestone_title: milestone.title)
+
       expect(page).to have_css(merge_request_css, count: 1)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
     end
   end
 
   context 'when a label filter has been applied' do
     it 'resets the label filter' do
       visit_merge_requests(project, label_name: bug.name)
+
       expect(page).to have_css(merge_request_css, count: 1)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
+    end
+  end
+
+  context 'when multiple label filters have been applied' do
+    let!(:label) { create(:label, project: project, name: 'Frontend') }
+    let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+    before do
+      visit_merge_requests(project)
+      init_label_search
+    end
+
+    it 'filters bug label' do
+      filtered_search.set('~bug')
+
+      filter_dropdown.find('.filter-dropdown-item', text: bug.title).click
+      init_label_search
+
+      expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible
+      expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
     end
   end
 
   context 'when a text search has been conducted' do
     it 'resets the text search filter' do
       visit_merge_requests(project, search: 'Bug')
+
       expect(page).to have_css(merge_request_css, count: 1)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
     end
   end
 
   context 'when author filter has been applied' do
     it 'resets the author filter' do
       visit_merge_requests(project, author_username: user.username)
+
       expect(page).to have_css(merge_request_css, count: 1)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
     end
   end
 
   context 'when assignee filter has been applied' do
     it 'resets the assignee filter' do
       visit_merge_requests(project, assignee_username: user.username)
+
       expect(page).to have_css(merge_request_css, count: 1)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
     end
   end
 
   context 'when all filters have been applied' do
-    it 'resets all filters' do
+    it 'clears all filters' do
       visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+
       expect(page).to have_css(merge_request_css, count: 0)
+      expect(get_filtered_search_placeholder).to eq('')
 
       reset_filters
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
     end
   end
 
   context 'when no filters have been applied' do
-    it 'the reset link should not be visible' do
+    it 'the clear button should not be visible' do
       visit_merge_requests(project)
+
       expect(page).to have_css(merge_request_css, count: 2)
+      expect(get_filtered_search_placeholder).to eq(default_placeholder)
       expect(page).not_to have_css(clear_search_css)
     end
   end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 2f3c3e45ae63082403ff987d251d23f40c3f89bc..a1f4eb2688b014f2bf17b8d3842f535d6bc8bc80 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
 
       it 'changes target_branch in new merge_request' do
         visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
-        click_button "Compare branches and continue"
 
         fill_in "merge_request_title", with: 'My brand new feature'
         fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 4ad944366c8285196df8831bf9b9e7745efcc71a..c2db7d8da3c719540bd300a12b535bf341f07b7a 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -3,8 +3,8 @@ require 'rails_helper'
 describe 'Merge request', :feature, :js do
   include WaitForAjax
 
-  let(:project) { create(:project) }
   let(:user) { create(:user) }
+  let(:project) { create(:project) }
   let(:merge_request) { create(:merge_request, source_project: project) }
 
   before do
@@ -31,13 +31,18 @@ describe 'Merge request', :feature, :js do
 
       wait_for_ajax
 
-      expect(page).to have_selector('.accept_merge_request')
+      expect(page).to have_selector('.accept-merge-request')
     end
   end
 
   context 'view merge request' do
     let!(:environment) { create(:environment, project: project) }
-    let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) }
+
+    let!(:deployment) do
+      create(:deployment, environment: environment,
+                          ref: 'feature',
+                          sha: merge_request.diff_head_sha)
+    end
 
     before do
       visit namespace_project_merge_request_path(project.namespace, project, merge_request)
@@ -51,6 +56,89 @@ describe 'Merge request', :feature, :js do
         expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
       end
     end
+
+    it 'shows green accept merge request button' do
+      # Wait for the `ci_status` and `merge_check` requests
+      wait_for_ajax
+      expect(page).to have_selector('.accept-merge-request.btn-create')
+    end
+  end
+
+  context 'view merge request with external CI service' do
+    before do
+      create(:service, project: project,
+                       active: true,
+                       type: 'CiService',
+                       category: 'ci')
+
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'has danger button while waiting for external CI status' do
+      # Wait for the `ci_status` and `merge_check` requests
+      wait_for_ajax
+      expect(page).to have_selector('.accept-merge-request.btn-danger')
+    end
+  end
+
+  context 'view merge request with failed GitLab CI pipelines' do
+    before do
+      commit_status = create(:commit_status, project: project, status: 'failed')
+      pipeline = create(:ci_pipeline, project: project,
+                                      sha: merge_request.diff_head_sha,
+                                      ref: merge_request.source_branch,
+                                      status: 'failed',
+                                      statuses: [commit_status])
+      create(:ci_build, :pending, pipeline: pipeline)
+
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'has danger button when not succeeded' do
+      # Wait for the `ci_status` and `merge_check` requests
+      wait_for_ajax
+      expect(page).to have_selector('.accept-merge-request.btn-danger')
+    end
+  end
+
+  context 'when merge request is in the blocked pipeline state' do
+    before do
+      create(:ci_pipeline, project: project,
+                           sha: merge_request.diff_head_sha,
+                           ref: merge_request.source_branch,
+                           status: :manual)
+
+      visit namespace_project_merge_request_path(project.namespace,
+                                                 project,
+                                                 merge_request)
+    end
+
+    it 'shows information about blocked pipeline' do
+      expect(page).to have_content("Pipeline blocked")
+      expect(page).to have_content(
+        "The pipeline for this merge request requires a manual action")
+      expect(page).to have_css('.ci-status-icon-manual')
+    end
+  end
+
+  context 'view merge request with MWBS button' do
+    before do
+      commit_status = create(:commit_status, project: project, status: 'pending')
+      pipeline = create(:ci_pipeline, project: project,
+                                      sha: merge_request.diff_head_sha,
+                                      ref: merge_request.source_branch,
+                                      status: 'pending',
+                                      statuses: [commit_status])
+      create(:ci_build, :pending, pipeline: pipeline)
+
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    it 'has info button when MWBS button' do
+      # Wait for the `ci_status` and `merge_check` requests
+      wait_for_ajax
+      expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info')
+    end
   end
 
   context 'merge error' do
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index a2e4054658888a3a5980f1d0a60aa524a568865d..c3297de709af6f1d2b6128c91b6c67724593149e 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -24,7 +24,7 @@ feature 'Milestone', feature: true do
       find('input[name="commit"]').click
 
       expect(find('.alert-success')).to have_content('Assign some issues to this milestone.')
-      expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016')
+      expect(page).to have_content('Nov 16, 2016–Dec 16, 2016')
     end
   end
 
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 7a562b5e03d079c4559c9df83136f8967a497320..e63feb14b7ea185255ea2607554c4dac012051d2 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do
   let(:user) { create(:user) }
 
   before do
-    login_as :user
+    login_as(user)
   end
 
   describe 'when signup is enabled' do
@@ -16,7 +16,7 @@ describe 'Profile account page', feature: true do
     it { expect(page).to have_content('Remove account') }
 
     it 'deletes the account' do
-      expect { click_link 'Delete account' }.to change { User.count }.by(-1)
+      expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1)
       expect(current_path).to eq(new_user_session_path)
     end
   end
@@ -61,4 +61,18 @@ describe 'Profile account page', feature: true do
       expect(find('#incoming-email-token').value).not_to eq(previous_token)
     end
   end
+
+  describe 'when I change my username' do
+    before do
+      visit profile_account_path
+    end
+
+    it 'changes my username' do
+      fill_in 'user_username', with: 'new-username'
+
+      click_button('Update username')
+
+      expect(page).to have_content('new-username')
+    end
+  end
 end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index eb1050d21c6665e5c2c203ac210dd56b9514be71..2f436f153aa5c193c45e106c8b15c741150e6e52 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
     scenario 'auto-populates the title', js: true do
       fill_in('Key', with: attributes_for(:key).fetch(:key))
 
-      expect(find_field('Title').value).to eq 'dummy@gitlab.com'
+      expect(page).to have_field("Title", with: "dummy@gitlab.com")
     end
 
     scenario 'saves the new key' do
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index eb7b8a246695dc03d942a7bbeaeef5628894556e..0917d4dc3ef8d426a4c4ed1cd4c542d2e917b2f3 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -4,11 +4,11 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
   let(:user) { create(:user) }
 
   def active_personal_access_tokens
-    find(".table.active-personal-access-tokens")
+    find(".table.active-tokens")
   end
 
   def inactive_personal_access_tokens
-    find(".table.inactive-personal-access-tokens")
+    find(".table.inactive-tokens")
   end
 
   def created_personal_access_token
@@ -26,7 +26,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
   end
 
   describe "token creation" do
-    it "allows creation of a token" do
+    it "allows creation of a personal access token" do
       name = FFaker::Product.brand
 
       visit profile_personal_access_tokens_path
@@ -43,7 +43,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
 
       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('In')
       expect(active_personal_access_tokens).to have_text('api')
       expect(active_personal_access_tokens).to have_text('read_user')
     end
@@ -60,6 +60,18 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
     end
   end
 
+  describe 'active tokens' do
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+    let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+    it 'only shows personal access tokens' do
+      visit profile_personal_access_tokens_path
+
+      expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+      expect(active_personal_access_tokens).not_to have_text(impersonation_token.name)
+    end
+  end
+
   describe "inactive tokens" do
     let!(:personal_access_token) { create(:personal_access_token, user: user) }
 
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
deleted file mode 100644
index e05fbb3715c07d36acd1c16cd7e9eea9c4f07594..0000000000000000000000000000000000000000
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do
-  let(:user) { create(:user) }
-
-  before do
-    login_as(user)
-  end
-
-  scenario 'User opts into receiving notifications about their own activity' do
-    visit profile_notifications_path
-
-    expect(page).not_to have_checked_field('user[notified_of_own_activity]')
-
-    check 'user[notified_of_own_activity]'
-
-    expect(page).to have_content('Notification settings saved')
-    expect(page).to have_checked_field('user[notified_of_own_activity]')
-  end
-
-  scenario 'User opts out of receiving notifications about their own activity' do
-    user.update!(notified_of_own_activity: true)
-    visit profile_notifications_path
-
-    expect(page).to have_checked_field('user[notified_of_own_activity]')
-
-    uncheck 'user[notified_of_own_activity]'
-
-    expect(page).to have_content('Notification settings saved')
-    expect(page).not_to have_checked_field('user[notified_of_own_activity]')
-  end
-end
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b47c6d431eba642c321325fccb9f5cd01931feb9
--- /dev/null
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Project Activity RSS' do
+  let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:path) { activity_namespace_project_path(project.namespace, project) }
+
+  before do
+    create(:issue, project: project)
+  end
+
+  context 'when signed in' do
+    before do
+      user = create(:user)
+      project.team << [user, :developer]
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button without a private token"
+  end
+end
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d94204230f617652d820e3c3b5ddb884226c78db
--- /dev/null
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, js: true do
+  include TreeHelper
+
+  let(:project) { create(:project, :public, :repository) }
+  let(:path) { 'CHANGELOG' }
+  let(:sha) { project.repository.commit.sha }
+
+  describe 'On a file(blob)' do
+    def get_absolute_url(path = "")
+      "http://#{page.server.host}:#{page.server.port}#{path}"
+    end
+
+    def visit_blob(fragment = nil)
+      visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+    end
+
+    describe 'Click "Permalink" button' do
+      it 'works with no initial line number fragment hash' do
+        visit_blob
+
+        expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))))
+      end
+
+      it 'maintains intitial fragment hash' do
+        fragment = "L3"
+
+        visit_blob(fragment)
+
+        expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)))
+      end
+
+      it 'changes fragment hash if line number clicked' do
+        ending_fragment = "L5"
+
+        visit_blob
+
+        find('#L3').click
+        find("##{ending_fragment}").click
+
+        expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+      end
+
+      it 'with initial fragment hash, changes fragment hash if line number clicked' do
+        fragment = "L1"
+        ending_fragment = "L5"
+
+        visit_blob(fragment)
+
+        find('#L3').click
+        find("##{ending_fragment}").click
+
+        expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+      end
+    end
+
+    describe 'Click "Blame" button' do
+      it 'works with no initial line number fragment hash' do
+        visit_blob
+
+        expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path))))
+      end
+
+      it 'maintains intitial fragment hash' do
+        fragment = "L3"
+
+        visit_blob(fragment)
+
+        expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: fragment)))
+      end
+
+      it 'changes fragment hash if line number clicked' do
+        ending_fragment = "L5"
+
+        visit_blob
+
+        find('#L3').click
+        find("##{ending_fragment}").click
+
+        expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+      end
+
+      it 'with initial fragment hash, changes fragment hash if line number clicked' do
+        fragment = "L1"
+        ending_fragment = "L5"
+
+        visit_blob(fragment)
+
+        find('#L3').click
+        find("##{ending_fragment}").click
+
+        expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03d08c1261268db58675f10fd7f3f422a99549c0
--- /dev/null
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'New blob creation', feature: true, js: true do
+  include WaitForAjax
+
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:project) { create(:project) }
+  given(:content) { 'class NextFeature\nend\n' }
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+    visit namespace_project_new_blob_path(project.namespace, project, 'master')
+  end
+
+  def edit_file
+    wait_for_ajax
+    fill_in 'file_name', with: 'feature.rb'
+    execute_script("ace.edit('editor').setValue('#{content}')")
+  end
+
+  def select_branch_index(index)
+    first('button.js-target-branch').click
+    wait_for_ajax
+    all('a[data-group="Branches"]')[index].click
+  end
+
+  def create_new_branch(name)
+    first('button.js-target-branch').click
+    click_link 'Create new branch'
+    fill_in 'new_branch_name', with: name
+    click_button 'Create'
+  end
+
+  def commit_file
+    click_button 'Commit Changes'
+  end
+
+  context 'with default target branch' do
+    background do
+      edit_file
+      commit_file
+    end
+
+    scenario 'creates the blob in the default branch' do
+      expect(page).to have_content 'master'
+      expect(page).to have_content 'successfully created'
+      expect(page).to have_content 'NextFeature'
+    end
+  end
+
+  context 'with different target branch' do
+    background do
+      edit_file
+      select_branch_index(0)
+      commit_file
+    end
+
+    scenario 'creates the blob in the different branch' do
+      expect(page).to have_content 'test'
+      expect(page).to have_content 'successfully created'
+    end
+  end
+
+  context 'with a new target branch' do
+    given(:new_branch_name) { 'new-feature' }
+
+    background do
+      edit_file
+      create_new_branch(new_branch_name)
+      commit_file
+    end
+
+    scenario 'creates the blob in the new branch' do
+      expect(page).to have_content new_branch_name
+      expect(page).to have_content 'successfully created'
+    end
+    scenario 'returns you to the mr' do
+      expect(page).to have_content 'New Merge Request'
+      expect(page).to have_content "From #{new_branch_name} into master"
+      expect(page).to have_content 'Add new file'
+    end
+  end
+
+  context 'the file already exist in the source branch' do
+    background do
+      Files::CreateService.new(
+        project,
+        user,
+        start_branch: 'master',
+        target_branch: 'master',
+        commit_message: 'Create file',
+        file_path: 'feature.rb',
+        file_content: content
+      ).execute
+      edit_file
+      commit_file
+    end
+
+    scenario 'shows error message' do
+      expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+      expect(page).to have_content('New File')
+      expect(page).to have_content('NextFeature')
+    end
+  end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index d26a0caf0368e3f10b26bb8a6080b7304c3fcf33..8e0306ce83b990f934de11350310df5cbf308f24 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -17,6 +17,14 @@ describe 'Branches', feature: true do
         repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
         expect(page).to have_content("Protected branches can be managed in project settings")
       end
+
+      it 'avoids a N+1 query in branches index' do
+        control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count
+
+        %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') }
+
+        expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+      end
     end
 
     describe 'Find branches' do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7baf7913424dcf7c085372a4521ada90113820f6..0b972d2a4392b00e8f7f147ecdc3b611912bb792 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -60,6 +60,7 @@ describe 'Cherry-pick Commits' do
         click_button 'Cherry-pick'
       end
       expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
+      expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
     end
   end
 
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..30a2b2bcf8c5dbd3e609a2b955139a3806843c42
--- /dev/null
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph in Commit View', :js, :feature do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+
+  before do
+    login_as(user)
+  end
+
+  context 'when commit has pipelines' do
+    let(:pipeline) do
+      create(:ci_empty_pipeline,
+              project: project,
+              ref: project.default_branch,
+              sha: project.commit.sha)
+    end
+
+    let(:build) do
+      create(:ci_build, pipeline: pipeline)
+    end
+
+    before do
+      build.run
+      visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+    end
+
+    it 'should display a mini pipeline graph' do
+      expect(page).to have_selector('.mr-widget-pipeline-graph')
+    end
+
+    it 'should show the builds list when stage is clicked' do
+      first('.mini-pipeline-graph-dropdown-toggle').click
+
+      wait_for_ajax
+
+      page.within '.js-builds-dropdown-list' do
+        expect(page).to have_selector('.ci-status-icon-running')
+        expect(page).to have_content(build.stage)
+      end
+    end
+  end
+
+  context 'when commit does not have pipelines' do
+    before do
+      visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+    end
+
+    it 'should not display a mini pipeline graph' do
+      expect(page).not_to have_selector('.mr-widget-pipeline-graph')
+    end
+  end
+end
diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6e0e1916f87cb3cac6c430e59b14c719ec852b1b
--- /dev/null
+++ b/spec/features/projects/commit/rss_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+feature 'Project Commits RSS' do
+  let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:path) { namespace_project_commits_path(project.namespace, project, :master) }
+
+  context 'when signed in' do
+    before do
+      user = create(:user)
+      project.team << [user, :developer]
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button with current_user's private token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button without a private token"
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e5866e7617ae27cb508f6730cb93464e..030043d14aa85233f68e104d95bbd7454b7aac0e 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -26,6 +26,14 @@ describe "Compare", js: true do
       click_button "Compare"
       expect(page).to have_content "Commits"
     end
+
+    it "filters branches" do
+      select_using_dropdown("from", "wip")
+
+      find(".js-compare-from-dropdown .compare-dropdown-toggle").click
+
+      expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3)
+    end
   end
 
   describe "tags" do
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index 0c51fe72ca4e4b97a6c77f65038c9c60ab64f2dd..2352329d58c3a77a70933cade72ccea2b0dafe19 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do
   end
 
   def expect_instructions_for(protocol)
-    msg = :"#{protocol.downcase}_url_to_repo"
-
-    expect(page).to have_content("git clone #{project.send(msg)}")
+    url = 
+      case protocol
+      when 'ssh'
+        project.ssh_url_to_repo
+      when 'http'
+        project.http_url_to_repo(developer)
+      end
+
+    expect(page).to have_content("git clone #{url}")
   end
 end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index a1643fd1f436de0ac68d7540c95c5a03713d3177..7c319af893bba054a8d07bca2915001c51e4b900 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do
         expect(page).to have_selector('.merge-requests-feature', visible: false)
       end
 
-      it 'hides merge requests section after save' do
-        select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
-
-        expect(page).to have_selector('.merge-requests-feature', visible: false)
-
-        click_button 'Save changes'
+      context 'given project with merge_requests_disabled access level' do
+        let(:project) { create(:project, :merge_requests_disabled) }
 
-        wait_for_ajax
-
-        expect(page).to have_selector('.merge-requests-feature', visible: false)
+        it 'hides merge requests section' do
+          expect(page).to have_selector('.merge-requests-feature', visible: false)
+        end
       end
     end
 
     context 'builds select' do
-      it 'hides merge requests section' do
+      it 'hides builds select section' do
         select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
 
         expect(page).to have_selector('.builds-feature', visible: false)
       end
 
-      it 'hides merge requests section after save' do
-        select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
-
-        expect(page).to have_selector('.builds-feature', visible: false)
+      context 'given project with builds_disabled access level' do
+        let(:project) { create(:project, :builds_disabled) }
 
-        click_button 'Save changes'
-
-        wait_for_ajax
-
-        expect(page).to have_selector('.builds-feature', visible: false)
+        it 'hides builds select section' do
+          expect(page).to have_selector('.builds-feature', visible: false)
+        end
       end
     end
   end
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee925e811e1b6d3a71d811c9bda15809eb5e3f10
--- /dev/null
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'Environment > Metrics', :feature do
+  include PrometheusHelpers
+
+  given(:user) { create(:user) }
+  given(:project) { create(:prometheus_project) }
+  given(:pipeline) { create(:ci_pipeline, project: project) }
+  given(:build) { create(:ci_build, pipeline: pipeline) }
+  given(:environment) { create(:environment, project: project) }
+  given(:current_time) { Time.now.utc }
+
+  background do
+    project.add_developer(user)
+    create(:deployment, environment: environment, deployable: build)
+    stub_all_prometheus_requests(environment.slug)
+
+    login_as(user)
+    visit_environment(environment)
+  end
+
+  around do |example|
+    Timecop.freeze(current_time) { example.run }
+  end
+
+  context 'with deployments and related deployable present' do
+    scenario 'shows metrics' do
+      click_link('See metrics')
+
+      expect(page).to have_css('svg.prometheus-graph')
+    end
+  end
+
+  def visit_environment(environment)
+    visit namespace_project_environment_path(environment.project.namespace,
+                                             environment.project,
+                                             environment)
+  end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
similarity index 84%
rename from spec/features/environment_spec.rb
rename to spec/features/projects/environments/environment_spec.rb
index c203e1f20c1636011abb30fc25cee9011a7ba4f6..e2d16e0830a0bac0cc5ae544b8854b3fc5a87dcd 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -13,7 +13,7 @@ feature 'Environment', :feature do
   feature 'environment details page' do
     given!(:environment) { create(:environment, project: project) }
     given!(:deployment) { }
-    given!(:manual) { }
+    given!(:action) { }
 
     before do
       visit_environment(environment)
@@ -37,13 +37,7 @@ feature 'Environment', :feature do
 
         scenario 'does show deployment SHA' do
           expect(page).to have_link(deployment.short_sha)
-        end
-
-        scenario 'does not show a re-deploy button for deployment without build' do
           expect(page).not_to have_link('Re-deploy')
-        end
-
-        scenario 'does not show terminal button' do
           expect(page).not_to have_terminal_button
         end
       end
@@ -58,28 +52,28 @@ feature 'Environment', :feature do
 
         scenario 'does show build name' do
           expect(page).to have_link("#{build.name} (##{build.id})")
-        end
-
-        scenario 'does show re-deploy button' do
           expect(page).to have_link('Re-deploy')
-        end
-
-        scenario 'does not show terminal button' do
           expect(page).not_to have_terminal_button
         end
 
         context 'with manual action' do
-          given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+          given(:action) do
+            create(:ci_build, :manual, pipeline: pipeline,
+                                       name: 'deploy to production')
+          end
 
           scenario 'does show a play button' do
-            expect(page).to have_link(manual.name.humanize)
+            expect(page).to have_link(action.name.humanize)
           end
 
           scenario 'does allow to play manual action' do
-            expect(manual).to be_skipped
-            expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
-            expect(page).to have_content(manual.name)
-            expect(manual.reload).to be_pending
+            expect(action).to be_manual
+
+            expect { click_link(action.name.humanize) }
+              .not_to change { Ci::Pipeline.count }
+
+            expect(page).to have_content(action.name)
+            expect(action.reload).to be_pending
           end
 
           context 'with external_url' do
@@ -111,9 +105,6 @@ feature 'Environment', :feature do
 
                 it 'displays a web terminal' do
                   expect(page).to have_selector('#terminal')
-                end
-
-                it 'displays a link to the environment external url' do
                   expect(page).to have_link(nil, href: environment.external_url)
                 end
               end
@@ -130,11 +121,15 @@ feature 'Environment', :feature do
 
           context 'when environment is available' do
             context 'with stop action' do
-              given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
-              given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+              given(:action) do
+                create(:ci_build, :manual, pipeline: pipeline,
+                                           name: 'close_app')
+              end
 
-              scenario 'does show stop button' do
-                expect(page).to have_link('Stop')
+              given(:deployment) do
+                create(:deployment, environment: environment,
+                                    deployable: build,
+                                    on_stop: 'close_app')
               end
 
               scenario 'does allow to stop environment' do
diff --git a/spec/features/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
similarity index 90%
rename from spec/features/environments_spec.rb
rename to spec/features/projects/environments/environments_spec.rb
index 78be7d36f473a7c0a46e3ebbef5f55ab92510032..641e2cf7402fbc6b33fae1551d0fbbead4de0e0e 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -12,7 +12,7 @@ feature 'Environments page', :feature, :js do
 
   given!(:environment) { }
   given!(:deployment) { }
-  given!(:manual) { }
+  given!(:action) { }
 
   before do
     visit_environments(project)
@@ -90,7 +90,7 @@ feature 'Environments page', :feature, :js do
         given(:pipeline) { create(:ci_pipeline, project: project) }
         given(:build) { create(:ci_build, pipeline: pipeline) }
 
-        given(:manual) do
+        given(:action) do
           create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
         end
 
@@ -102,19 +102,17 @@ feature 'Environments page', :feature, :js do
 
         scenario 'does show a play button' do
           find('.js-dropdown-play-icon-container').click
-          expect(page).to have_content(manual.name.humanize)
+          expect(page).to have_content(action.name.humanize)
         end
 
         scenario 'does allow to play manual action', js: true do
-          expect(manual).to be_skipped
+          expect(action).to be_manual
 
           find('.js-dropdown-play-icon-container').click
-          expect(page).to have_content(manual.name.humanize)
+          expect(page).to have_content(action.name.humanize)
 
-          expect { click_link(manual.name.humanize) }
+          expect { find('.js-manual-action-link').click }
             .not_to change { Ci::Pipeline.count }
-
-          expect(manual.reload).to be_pending
         end
 
         scenario 'does show build name and id' do
@@ -144,17 +142,18 @@ feature 'Environments page', :feature, :js do
         end
 
         context 'with stop action' do
-          given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
-          given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
-
-          scenario 'does show stop button' do
-            expect(page).to have_selector('.stop-env-link')
+          given(:action) do
+            create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
           end
 
-          scenario 'starts build when stop button clicked' do
-            find('.stop-env-link').click
+          given(:deployment) do
+            create(:deployment, environment: environment,
+                                deployable: build,
+                                on_stop: 'close_app')
+          end
 
-            expect(page).to have_content('close_app')
+          scenario 'does show stop button' do
+            expect(page).to have_selector('.stop-env-link')
           end
 
           context 'for reporter' do
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index 69295e450d01611d6255d69c0eb38b571024f901..d281043caa3c9e9c0cf369c295840d21ce0a6e97 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'user checks git blame', feature: true do
+feature 'user browses project', feature: true do
   let(:project) { create(:project) }
   let(:user) { create(:user) }
 
@@ -18,4 +18,16 @@ feature 'user checks git blame', feature: true do
     expect(page).to have_content "Dmitriy Zaporozhets"
     expect(page).to have_content "Initial commit"
   end
+
+  scenario 'can see raw content of LFS pointer with LFS disabled' do
+    allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+    click_link 'files'
+    click_link 'lfs'
+    click_link 'lfs_object.iso'
+
+    expect(page).not_to have_content 'Download (1.5 MB)'
+    expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
+    expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
+    expect(page).to have_content 'size 1575078'
+  end
 end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 64094af29c09cfaf640b49c33a3a27995d24ad10..ccadc936567294395633eba154bf966f5c6a4d4a 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do
   let(:project_master) { create(:user) }
   let(:project) { create(:project) }
   background do
-    project.repository.remove_file(project_master, 'LICENSE',
+    project.repository.delete_file(project_master, 'LICENSE',
       message: 'Remove LICENSE', branch_name: 'master')
     project.team << [project_master, :master]
     login_as(project_master)
@@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do
     select_template('MIT License')
 
     file_content = first('.file-editor')
-    expect(file_content).to have_content('The MIT License (MIT)')
+    expect(file_content).to have_content('MIT License')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
     fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do
 
     expect(current_path).to eq(
       namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
-    expect(page).to have_content('The MIT License (MIT)')
+    expect(page).to have_content('MIT License')
     expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
   end
 
@@ -49,7 +49,7 @@ feature 'project owner creates a license file', feature: true, js: true do
     select_template('MIT License')
 
     file_content = first('.file-editor')
-    expect(file_content).to have_content('The MIT License (MIT)')
+    expect(file_content).to have_content('MIT License')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
     fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -57,7 +57,7 @@ feature 'project owner creates a license file', feature: true, js: true do
 
     expect(current_path).to eq(
       namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
-    expect(page).to have_content('The MIT License (MIT)')
+    expect(page).to have_content('MIT License')
     expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
   end
 
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 4453b6d485fef004d9643c74c6514dfd008a7685..420db962318926072e7c2182ca66875be95f5854 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f
     select_template('MIT License')
 
     file_content = first('.file-editor')
-    expect(file_content).to have_content('The MIT License (MIT)')
+    expect(file_content).to have_content('MIT License')
     expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
 
     fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -34,7 +34,7 @@ feature 'project owner sees a link to create a license file in empty project', f
 
     expect(current_path).to eq(
       namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
-    expect(page).to have_content('The MIT License (MIT)')
+    expect(page).to have_content('MIT License')
     expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
   end
 
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
index 8120a51c5157dd6d8eb2c8b4065e98b4989a2147..726469daba4475fa35146a15f24f170879ca3ec8 100644
--- a/spec/features/projects/guest_navigation_menu_spec.rb
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -15,13 +15,11 @@ describe "Guest navigation menu" do
 
     within(".nav-links") do
       expect(page).to have_content 'Project'
-      expect(page).to have_content 'Activity'
       expect(page).to have_content 'Issues'
       expect(page).to have_content 'Wiki'
 
       expect(page).not_to have_content 'Repository'
       expect(page).not_to have_content 'Pipelines'
-      expect(page).not_to have_content 'Graphs'
       expect(page).not_to have_content 'Merge Requests'
     end
   end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 16dddb2a86b92292645d45a4fb24a2f4d363701f..40caf89dd54c4bdce4b18c3f7ee9fba0455c6867 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -9,7 +9,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
   include ExportFileHelper
 
   let(:user) { create(:admin) }
-  let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+  let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
   let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
 
   let(:sensitive_words) { %w[pass secret token key] }
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 3015576f6f8561322936f33ade8be3a77527b4d4..2d1106ea3e80460588d3fa93e2a50e57c90f2b89 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -4,7 +4,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
   include Select2Helper
 
   let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
-  let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+  let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
 
   background do
     allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index d0bafc6168cf7aeb44b73c9d416e8b54e738b20c..cb399ea55df4c46b6c770a8f25f210b900046881 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
-  let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+  let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
   let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
 
   let(:project) { create(:empty_project) }
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 20cdfbae24fefb2424cd4b4045d233d1601e0db1..399c1d478c5bf7e6e71905710c0c80e25f430a9f 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/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index e90a033b8c41fdf4579e3a5b653761de90812ee0..62d0aedda48b5e3cf4584ecf4d5aceff55ec98f7 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do
     let(:description_addition) { ' appending to description' }
 
     background do
-      project.repository.commit_file(
+      project.repository.create_file(
         user,
         '.gitlab/issue_templates/bug.md',
         template_content,
         message: 'added issue template',
-        branch_name: 'master',
-        update: false)
-      project.repository.commit_file(
+        branch_name: 'master')
+      project.repository.create_file(
         user,
         '.gitlab/issue_templates/test.md',
         longtemplate_content,
         message: 'added issue template',
-        branch_name: 'master',
-        update: false)
+        branch_name: 'master')
       visit edit_namespace_project_issue_path project.namespace, project, issue
       fill_in :'issue[title]', with: 'test issue title'
     end
@@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do
     let(:issue) { create(:issue, author: user, assignee: user, project: project) }
 
     background do
-      project.repository.commit_file(
+      project.repository.create_file(
         user,
         '.gitlab/issue_templates/bug.md',
         template_content,
         message: 'added issue template',
-        branch_name: 'master',
-        update: false)
+        branch_name: 'master')
       visit edit_namespace_project_issue_path project.namespace, project, issue
       fill_in :'issue[title]', with: 'test issue title'
       fill_in :'issue[description]', with: prior_description
@@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do
     let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
 
     background do
-      project.repository.commit_file(
+      project.repository.create_file(
         user,
         '.gitlab/merge_request_templates/feature-proposal.md',
         template_content,
         message: 'added merge request template',
-        branch_name: 'master',
-        update: false)
+        branch_name: 'master')
       visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
       fill_in :'merge_request[title]', with: 'test merge request title'
     end
@@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do
       fork_project.team << [fork_user, :master]
       create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
       login_as fork_user
-      project.repository.commit_file(
+      project.repository.create_file(
         fork_user,
         '.gitlab/merge_request_templates/feature-proposal.md',
         template_content,
         message: 'added merge request template',
-        branch_name: 'master',
-        update: false)
+        branch_name: 'master')
       visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
       fill_in :'merge_request[title]', with: 'test merge request title'
     end
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..71429f000959af4a948c7c230a77873713e41641
--- /dev/null
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+feature 'Project Issues RSS' do
+  let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:path) { namespace_project_issues_path(project.namespace, project) }
+
+  before do
+    create(:issue, project: project)
+  end
+
+  context 'when signed in' do
+    before do
+      user = create(:user)
+      project.team << [user, :developer]
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button with current_user's private token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button without a private token"
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 81b0c991d4fb586513d054f0c31392b76815ca81..e2911a37e408c46ca8e02bcb69d78985e4f6baf0 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -29,15 +29,15 @@ feature 'Issue prioritization', feature: true do
       issue_1.labels << label_5
 
       login_as user
-      visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+      visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
 
       # Ensure we are indicating that issues are sorted by priority
-      expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+      expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
 
       page.within('.issues-holder') do
         issue_titles = all('.issues-list .issue-title-text').map(&:text)
 
-        expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1'])
+        expect(issue_titles).to eq(%w(issue_4 issue_3 issue_5 issue_2 issue_1))
       end
     end
   end
@@ -68,16 +68,16 @@ feature 'Issue prioritization', feature: true do
       issue_6.labels << label_5 # 8 - No priority
 
       login_as user
-      visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+      visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
 
-      expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+      expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
 
       page.within('.issues-holder') do
         issue_titles = all('.issues-list .issue-title-text').map(&:text)
 
         expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8')
         expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7')
-        expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6'])
+        expect(issue_titles[5..-1]).to eq(%w(issue_2 issue_4 issue_6))
       end
     end
   end
diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b1a3af612a17ad88c55b9f6ce78c882a134025ea
--- /dev/null
+++ b/spec/features/projects/main/rss_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Project RSS' do
+  let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:path) { namespace_project_path(project.namespace, project) }
+
+  context 'when signed in' do
+    before do
+      user = create(:user)
+      project.team << [user, :developer]
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index d6ebb523f9533aa72292925f662fc7c874970ffa..c7a32a65e499887dcb1c171a7b95eb64dd651fd7 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -85,7 +85,7 @@ feature 'Projects > Members > Sorting', feature: true do
   end
 
   def visit_members_list(sort:)
-    visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+    visit namespace_project_project_members_path(project.namespace.to_param, project, sort: sort)
   end
 
   def first_member
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0b4dcaa39c693a264d1732569debc43e06d5d682..b64c15e0adc848fff99a52238c134a66a3b953a2 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -57,6 +57,12 @@ feature 'Projects > Members > User requests access', feature: true do
   end
 
   def open_project_settings_menu
-    find('#project-settings-button').click
+    page.within('.layout-nav .nav-links') do
+      click_link('Settings')
+    end
+
+    page.within('.page-with-layout-nav .sub-nav') do
+      click_link('Members')
+    end
   end
 end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..df229d0aa78c9c5b58153dfe2738e9e6b2b9e502
--- /dev/null
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+feature 'Project milestone', :feature do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) }
+  let(:milestone) { create(:milestone, project: project) }
+
+  before do
+    login_as(user)
+  end
+
+  context 'when project has enabled issues' do
+    before do
+      visit namespace_project_milestone_path(project.namespace, project, milestone)
+    end
+
+    it 'shows issues tab' do
+      within('#content-body') do
+        expect(page).to have_link 'Issues', href: '#tab-issues'
+        expect(page).to have_selector '.nav-links li.active', count: 1
+        expect(find('.nav-links li.active')).to have_content 'Issues'
+      end
+    end
+
+    it 'shows issues stats' do
+      expect(page).to have_content 'issues:'
+    end
+
+    it 'shows Browse Issues button' do
+      within('#content-body') do
+        expect(page).to have_link 'Browse Issues'
+      end
+    end
+  end
+
+  context 'when project has disabled issues' do
+    before do
+      project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+      visit namespace_project_milestone_path(project.namespace, project, milestone)
+    end
+
+    it 'hides issues tab' do
+      within('#content-body') do
+        expect(page).not_to have_link 'Issues', href: '#tab-issues'
+        expect(page).to have_selector '.nav-links li.active', count: 1
+        expect(find('.nav-links li.active')).to have_content 'Merge Requests'
+      end
+    end
+
+    it 'hides issues stats' do
+      expect(page).to have_no_content 'issues:'
+    end
+
+    it 'hides Browse Issues button' do
+      within('#content-body') do
+        expect(page).not_to have_link 'Browse Issues'
+      end
+    end
+
+    it 'does not show an informative message' do
+      expect(page).not_to have_content('Assign some issues to this milestone.')
+    end
+  end
+end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 45185f2dd1f1936149bab981ee5387c0810a17cb..52196ce49bd2059285a5ce62626ef99da271295c 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -16,6 +16,15 @@ feature "New project", feature: true do
 
         expect(find_field("project_visibility_level_#{level}")).to be_checked
       end
+
+      it 'saves visibility level on validation error' do
+        visit new_project_path
+
+        choose(key)
+        click_button('Create project')
+
+        expect(find_field("project_visibility_level_#{level}")).to be_checked
+      end
     end
   end
 
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 8d1214dedb4de4039d33b3d1e932406d29a65cdc..162056671e08aa43f47971343e257d440062280b 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -26,22 +26,66 @@ describe 'Pipelines', :feature, :js do
         )
       end
 
-      [:all, :running, :branches].each do |scope|
-        context "when displaying #{scope}" do
-          before do
-            visit_project_pipelines(scope: scope)
-          end
+      context 'scope' do
+        before do
+          create(:ci_empty_pipeline, status: 'pending', project: project, sha: project.commit.id, ref: 'master')
+          create(:ci_empty_pipeline, status: 'running', project: project, sha: project.commit.id, ref: 'master')
+          create(:ci_empty_pipeline, status: 'created', project: project, sha: project.commit.id, ref: 'master')
+          create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
+        end
 
-          it 'contains pipeline commit short SHA' do
-            expect(page).to have_content(pipeline.short_sha)
-          end
+        [:all, :running, :pending, :finished, :branches].each do |scope|
+          context "when displaying #{scope}" do
+            before do
+              visit_project_pipelines(scope: scope)
+            end
 
-          it 'contains branch name' do
-            expect(page).to have_content(pipeline.ref)
+            it 'contains pipeline commit short SHA' do
+              expect(page).to have_content(pipeline.short_sha)
+            end
+
+            it 'contains branch name' do
+              expect(page).to have_content(pipeline.ref)
+            end
           end
         end
       end
 
+      context 'header tabs' do
+        before do
+          visit namespace_project_pipelines_path(project.namespace, project)
+          wait_for_vue_resource
+        end
+
+        it 'shows a tab for All pipelines and count' do
+          expect(page.find('.js-pipelines-tab-all a').text).to include('All')
+          expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
+        end
+
+        it 'shows a tab for Pending pipelines and count' do
+          expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending')
+          expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
+        end
+
+        it 'shows a tab for Running pipelines and count' do
+          expect(page.find('.js-pipelines-tab-running a').text).to include('Running')
+          expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
+        end
+
+        it 'shows a tab for Finished pipelines and count' do
+          expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished')
+          expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
+        end
+
+        it 'shows a tab for Branches' do
+          expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches')
+        end
+
+        it 'shows a tab for Tags' do
+          expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags')
+        end
+      end
+
       context 'when pipeline is cancelable' do
         let!(:build) do
           create(:ci_build, pipeline: pipeline,
@@ -55,15 +99,18 @@ describe 'Pipelines', :feature, :js do
         end
 
         it 'indicates that pipeline can be canceled' do
-          expect(page).to have_link('Cancel')
+          expect(page).to have_selector('.js-pipelines-cancel-button')
           expect(page).to have_selector('.ci-running')
         end
 
         context 'when canceling' do
-          before { click_link('Cancel') }
+          before do
+            find('.js-pipelines-cancel-button').click
+            wait_for_vue_resource
+          end
 
           it 'indicated that pipelines was canceled' do
-            expect(page).not_to have_link('Cancel')
+            expect(page).not_to have_selector('.js-pipelines-cancel-button')
             expect(page).to have_selector('.ci-canceled')
           end
         end
@@ -82,15 +129,18 @@ describe 'Pipelines', :feature, :js do
         end
 
         it 'indicates that pipeline can be retried' do
-          expect(page).to have_link('Retry')
+          expect(page).to have_selector('.js-pipelines-retry-button')
           expect(page).to have_selector('.ci-failed')
         end
 
         context 'when retrying' do
-          before { click_link('Retry') }
+          before do
+            find('.js-pipelines-retry-button').click
+            wait_for_vue_resource
+          end
 
           it 'shows running pipeline that is not retryable' do
-            expect(page).not_to have_link('Retry')
+            expect(page).not_to have_selector('.js-pipelines-retry-button')
             expect(page).to have_selector('.ci-running')
           end
         end
@@ -132,17 +182,17 @@ describe 'Pipelines', :feature, :js do
         it 'has link to the manual action' do
           find('.js-pipeline-dropdown-manual-actions').click
 
-          expect(page).to have_link('manual build')
+          expect(page).to have_button('manual build')
         end
 
         context 'when manual action was played' do
           before do
             find('.js-pipeline-dropdown-manual-actions').click
-            click_link('manual build')
+            click_button('manual build')
           end
 
           it 'enqueues manual action job' do
-            expect(manual.reload).to be_pending
+            expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
           end
         end
       end
@@ -159,7 +209,7 @@ describe 'Pipelines', :feature, :js do
           before { visit_project_pipelines }
 
           it 'is cancelable' do
-            expect(page).to have_link('Cancel')
+            expect(page).to have_selector('.js-pipelines-cancel-button')
           end
 
           it 'has pipeline running' do
@@ -167,10 +217,10 @@ describe 'Pipelines', :feature, :js do
           end
 
           context 'when canceling' do
-            before { click_link('Cancel') }
+            before { find('.js-pipelines-cancel-button').trigger('click') }
 
             it 'indicates that pipeline was canceled' do
-              expect(page).not_to have_link('Cancel')
+              expect(page).not_to have_selector('.js-pipelines-cancel-button')
               expect(page).to have_selector('.ci-canceled')
             end
           end
@@ -189,7 +239,7 @@ describe 'Pipelines', :feature, :js do
           end
 
           it 'is not retryable' do
-            expect(page).not_to have_link('Retry')
+            expect(page).not_to have_selector('.js-pipelines-retry-button')
           end
 
           it 'has failed pipeline' do
@@ -284,6 +334,18 @@ describe 'Pipelines', :feature, :js do
             expect(build.reload).to be_canceled
           end
         end
+
+        context 'dropdown jobs list' do
+          it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
+            find('.js-builds-dropdown-button').trigger('click')
+
+            execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
+
+            find('.mini-pipeline-graph-dropdown-item').trigger('click')
+
+            expect(page).to have_selector('.js-ci-action-icon')
+          end
+        end
       end
 
       context 'with pagination' do
@@ -315,8 +377,14 @@ describe 'Pipelines', :feature, :js do
         visit new_namespace_project_pipeline_path(project.namespace, project)
       end
 
-      context 'for valid commit' do
-        before { fill_in('pipeline[ref]', with: 'master') }
+      context 'for valid commit', js: true do
+        before do
+          click_button project.default_branch
+
+          page.within '.dropdown-menu' do
+            click_link 'master'
+          end
+        end
 
         context 'with gitlab-ci.yml' do
           before { stub_ci_pipeline_to_return_yaml_file }
@@ -333,15 +401,6 @@ describe 'Pipelines', :feature, :js do
           it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
         end
       end
-
-      context 'for invalid commit' do
-        before do
-          fill_in('pipeline[ref]', with: 'invalid-reference')
-          click_on 'Create pipeline'
-        end
-
-        it { expect(page).to have_content('Reference not found') }
-      end
     end
 
     describe 'Create pipelines' do
@@ -353,18 +412,22 @@ describe 'Pipelines', :feature, :js do
 
       describe 'new pipeline page' do
         it 'has field to add a new pipeline' do
-          expect(page).to have_field('pipeline[ref]')
+          expect(page).to have_selector('.js-branch-select')
+          expect(find('.js-branch-select')).to have_content project.default_branch
           expect(page).to have_content('Create for')
         end
       end
 
       describe 'find pipelines' do
         it 'shows filtered pipelines', js: true do
-          fill_in('pipeline[ref]', with: 'fix')
-          find('input#ref').native.send_keys(:keydown)
+          click_button project.default_branch
+
+          page.within '.dropdown-menu' do
+            find('.dropdown-input-field').native.send_keys('fix')
 
-          within('.ui-autocomplete') do
-            expect(page).to have_selector('li', text: 'fix')
+            page.within '.dropdown-content' do
+              expect(page).to have_content('fix')
+            end
           end
         end
       end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f5adb53a2dc0be16fe3e85b2b4249bda437c6549..24d22a092d43e9684f6dc51b5590ad2666805f9a 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'Setup Mattermost slash commands', feature: true do
+feature 'Setup Mattermost slash commands', :feature, :js do
   let(:user) { create(:user) }
   let(:project) { create(:empty_project) }
   let(:service) { project.create_mattermost_slash_commands_service }
@@ -62,11 +62,11 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      team_name = teams.first[1]['display_name']
-      select_element = find('select#mattermost_team_id')
+      team_name = teams.first['display_name']
+      select_element = find('#mattermost_team_id')
       selected_option = select_element.find('option[selected]')
 
-      expect(select_element['disabled']).to eq('disabled')
+      expect(select_element['disabled']).to be(true)
       expect(selected_option).to have_content(team_name.to_s)
     end
 
@@ -75,7 +75,7 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s)
+      expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id'])
     end
 
     it 'shows an explanation user is a member of multiple teams' do
@@ -92,12 +92,9 @@ feature 'Setup Mattermost slash commands', feature: true do
 
       click_link 'Add to Mattermost'
 
-      select_element = find('select#mattermost_team_id')
-      selected_option = select_element.find('option[selected]')
+      select_element = find('#mattermost_team_id')
 
-      expect(select_element['disabled']).to be(nil)
-      expect(selected_option).to have_content('Select team...')
-      # The 'Select team...' placeholder is item `0`.
+      expect(select_element['disabled']).to be(false)
       expect(select_element.all('option').count).to eq(3)
     end
 
@@ -110,20 +107,37 @@ feature 'Setup Mattermost slash commands', feature: true do
       expect(page).to have_content('test mattermost error message')
     end
 
+    it 'enables the submit button if the required fields are provided', :js do
+      stub_teams(count: 1)
+
+      click_link 'Add to Mattermost'
+
+      expect(find('input[type="submit"]')['disabled']).not_to be(true)
+    end
+
+    it 'disables the submit button if the required fields are not provided', :js do
+      stub_teams(count: 1)
+
+      click_link 'Add to Mattermost'
+
+      fill_in('mattermost_trigger', with: '')
+
+      expect(find('input[type="submit"]')['disabled']).to be(true)
+    end
+
     def stub_teams(count: 0)
       teams = create_teams(count)
 
-      allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams }
+      allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
 
       teams
     end
 
     def create_teams(count = 0)
-      teams = {}
+      teams = []
 
       count.times do |i|
-        i += 1
-        teams[i] = { id: i, display_name: i }
+        teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" })
       end
 
       teams
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98cb9fc6e54b9ba6189db1929325efb..c0a4a1e4bf5196e14d18a012c211c7007c701722 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
 
   background do
     service.fields
-    service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+    service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
     project.team << [user, :master]
     login_as(user)
   end
@@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
     expect(page.find_field("service_merge_request_channel").value).to have_content '3'
     expect(page.find_field("service_note_channel").value).to have_content '4'
     expect(page.find_field("service_tag_push_channel").value).to have_content '5'
-    expect(page.find_field("service_build_channel").value).to have_content '6'
+    expect(page.find_field("service_pipeline_channel").value).to have_content '6'
     expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
   end
 end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 6815039d5edadf42fe7f3336b603844280abc437..321af416c9146a32ff6c47c718e3c5592119f24e 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -62,4 +62,27 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
       expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
     end
   end
+
+  describe 'Checkbox to enable merge request link' do
+    before do
+      visit edit_project_path(project)
+    end
+
+    scenario 'is initially checked' do
+      checkbox = find_field('project_printing_merge_request_link_enabled')
+      expect(checkbox).to be_checked
+    end
+
+    scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do
+      uncheck('project_printing_merge_request_link_enabled')
+      click_on('Save')
+
+      # Wait for save to complete and page to reload
+      checkbox = find_field('project_printing_merge_request_link_enabled')
+      expect(checkbox).not_to be_checked
+
+      project.reload
+      expect(project.printing_merge_request_link_enabled).to be(false)
+    end
+  end
 end
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ac51997d65374543ce71c27ccf0e25b14f80aa4
--- /dev/null
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Project Tree RSS' do
+  let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:path) { namespace_project_tree_path(project.namespace, project, :master) }
+
+  context 'when signed in' do
+    before do
+      user = create(:user)
+      project.team << [user, :developer]
+      login_as(user)
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "an autodiscoverable RSS feed without a private token"
+  end
+end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6825b95c8aa3ddfb153636cb6d00c0701ae620fc
--- /dev/null
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views Git access wiki page', :feature do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:wiki_page) do
+    WikiPages::CreateService.new(
+      project,
+      user,
+      title: 'home',
+      content: '[some link](other-page)'
+    ).execute
+  end
+
+  before do
+    login_as(user)
+  end
+
+  scenario 'Visit Wiki Page Current Commit' do
+    visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+    click_link 'Clone repository'
+    expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}")
+    expect(page).to have_text(project.wiki.http_url_to_repo(user))
+  end
+end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa962e66d145f09e3dd73fa4273df3394..aedc0333cb9a2fb29f3e9196363dc959ba8eee46 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
   context 'in the user namespace' do
     let(:project) { create(:project, namespace: user.namespace) }
 
-    scenario 'the home page' do
-      click_link 'Edit'
-
-      fill_in :wiki_content, with: 'My awesome wiki!'
-      click_button 'Save changes'
-
-      expect(page).to have_content('Home')
-      expect(page).to have_content("Last edited by #{user.name}")
-      expect(page).to have_content('My awesome wiki!')
+    context 'the home page' do
+      scenario 'success when the wiki content is not empty' do
+        click_link 'Edit'
+
+        fill_in :wiki_content, with: 'My awesome wiki!'
+        click_button 'Save changes'
+
+        expect(page).to have_content('Home')
+        expect(page).to have_content("Last edited by #{user.name}")
+        expect(page).to have_content('My awesome wiki!')
+      end
+
+      scenario 'failure when the wiki content is empty' do
+        click_link 'Edit'
+
+        fill_in :wiki_content, with: ''
+        click_button 'Save changes'
+
+        expect(page).to have_selector('.wiki-form')
+        expect(page).to have_content('Edit Page')
+        expect(page).to have_content('The form contains the following error:')
+        expect(page).to have_content('Content can\'t be blank')
+        expect(find('textarea#wiki_content').value).to eq ''
+      end
     end
   end
 
diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c17e06612de383f645c8efaeaa0233bc97718ea6
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Projects > Wiki > User views the wiki page', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:old_page_version_id) { wiki_page.versions.last.id }
+  let(:wiki_page) do
+    WikiPages::CreateService.new(
+      project,
+      user,
+      title: 'home',
+      content: '[some link](other-page)'
+    ).execute
+  end
+
+  background do
+    project.team << [user, :master]
+    login_as(user)
+    WikiPages::UpdateService.new(
+      project,
+      user,
+      message: 'updated home',
+      content: 'updated [some link](other-page)',
+      format: :markdown
+    ).execute(wiki_page)
+  end
+
+  scenario 'Visit Wiki Page Current Commit' do
+    visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+    expect(page).to have_selector('a.btn', text: 'Edit')
+  end
+
+  scenario 'Visit Wiki Page Historical Commit' do
+    visit namespace_project_wiki_path(
+      project.namespace,
+      project,
+      wiki_page,
+      version_id: old_page_version_id
+    )
+
+    expect(page).not_to have_selector('a.btn', text: 'Edit')
+  end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c30d38b650891b5cbfbcf5f448d1f9cd2cb770fe..ba56030e28d2ccc6ef17b67716ba7701cb6853e7 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -18,7 +18,7 @@ feature 'Project', feature: true do
     it 'passes through html-pipeline' do
       project.update_attribute(:description, 'This project is the :poop:')
       visit path
-      expect(page).to have_css('.project-home-desc > p > img')
+      expect(page).to have_css('.project-home-desc > p > gl-emoji')
     end
 
     it 'sanitizes unwanted tags' do
@@ -56,7 +56,7 @@ feature 'Project', feature: true do
   end
 
   describe 'removal', js: true do
-    let(:user)    { create(:user) }
+    let(:user)    { create(:user, username: 'test', name: 'test') }
     let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
 
     before do
@@ -67,7 +67,7 @@ feature 'Project', feature: true do
 
     it 'removes a project' do
       expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
-      expect(page).to have_content "Project 'project1' will be deleted."
+      expect(page).to have_content "Project 'test / project1' will be deleted."
       expect(Project.all.count).to be_zero
       expect(project.issues).to be_empty
       expect(project.merge_requests).to be_empty
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 7da05defa81fd0871786eba6eb87a9aa8600de71..a6560a810960a551cad80468e5631d2946a022de 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,7 @@
 require 'spec_helper'
 
 describe "Search", feature: true  do
+  include FilteredSearchHelpers
   include WaitForAjax
 
   let(:user) { create(:user) }
@@ -170,7 +171,8 @@ describe "Search", feature: true  do
           sleep 2
 
           expect(page).to have_selector('.filtered-search')
-          expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+          expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+          expect_filtered_search_input_empty
         end
 
         it 'takes user to her issues page when issues authored is clicked' do
@@ -178,7 +180,8 @@ describe "Search", feature: true  do
           sleep 2
 
           expect(page).to have_selector('.filtered-search')
-          expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+          expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+          expect_filtered_search_input_empty
         end
 
         it 'takes user to her MR page when MR assigned is clicked' do
@@ -186,7 +189,8 @@ describe "Search", feature: true  do
           sleep 2
 
           expect(page).to have_selector('.merge-requests-holder')
-          expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+          expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+          expect_filtered_search_input_empty
         end
 
         it 'takes user to her MR page when MR authored is clicked' do
@@ -194,7 +198,8 @@ describe "Search", feature: true  do
           sleep 2
 
           expect(page).to have_selector('.merge-requests-holder')
-          expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+          expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+          expect_filtered_search_input_empty
         end
       end
 
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 4e7a2c0ecc031687a5d9457c9e45d9233d4b8fea..350de2e5b6b30c3f932e34bec5755f709f39913c 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -110,6 +110,20 @@ describe "Internal Project Access", feature: true  do
     it { is_expected.to be_denied_for(:external) }
   end
 
+  describe "GET /:project_path/settings/repository" do
+    subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+    it { is_expected.to be_allowed_for(:admin) }
+    it { is_expected.to be_allowed_for(:owner).of(project) }
+    it { is_expected.to be_allowed_for(:master).of(project) }
+    it { is_expected.to be_denied_for(:developer).of(project) }
+    it { is_expected.to be_denied_for(:reporter).of(project) }
+    it { is_expected.to be_denied_for(:guest).of(project) }
+    it { is_expected.to be_denied_for(:user) }
+    it { is_expected.to be_denied_for(:visitor) }
+    it { is_expected.to be_denied_for(:external) }
+  end
+
   describe "GET /:project_path/blob" do
     let(:commit) { project.repository.commit }
     subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index c74cdc05593e49a4898bf7cb50ac887c79ea8442..6236420644092e3a1745eceb842d33eb2531ddab 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -110,6 +110,20 @@ describe "Private Project Access", feature: true  do
     it { is_expected.to be_denied_for(:external) }
   end
 
+  describe "GET /:project_path/settings/repository" do
+    subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+    it { is_expected.to be_allowed_for(:admin) }
+    it { is_expected.to be_allowed_for(:owner).of(project) }
+    it { is_expected.to be_allowed_for(:master).of(project) }
+    it { is_expected.to be_denied_for(:developer).of(project) }
+    it { is_expected.to be_denied_for(:reporter).of(project) }
+    it { is_expected.to be_denied_for(:guest).of(project) }
+    it { is_expected.to be_denied_for(:user) }
+    it { is_expected.to be_denied_for(:external) }
+    it { is_expected.to be_denied_for(:visitor) }
+  end
+
   describe "GET /:project_path/blob" do
     let(:commit) { project.repository.commit }
     subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 485ef335b787dd5631c34a9e94a80bcb542d4ae8..0e0c3140fd057e44a858f15c6476c110d0c2fc2f 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -110,6 +110,20 @@ describe "Public Project Access", feature: true  do
     it { is_expected.to be_denied_for(:external) }
   end
 
+  describe "GET /:project_path/settings/repository" do
+    subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+    it { is_expected.to be_allowed_for(:admin) }
+    it { is_expected.to be_allowed_for(:owner).of(project) }
+    it { is_expected.to be_allowed_for(:master).of(project) }
+    it { is_expected.to be_denied_for(:developer).of(project) }
+    it { is_expected.to be_denied_for(:reporter).of(project) }
+    it { is_expected.to be_denied_for(:guest).of(project) }
+    it { is_expected.to be_denied_for(:user) }
+    it { is_expected.to be_denied_for(:visitor) }
+    it { is_expected.to be_denied_for(:external) }
+  end
+
   describe "GET /:project_path/pipelines" do
     subject { namespace_project_pipelines_path(project.namespace, project) }
 
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 0f30f562539ba6ad02d146281126337e1e724164..ccfafe6db7d5d3427adad86d079ab7bd91b4f89f 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,16 +10,12 @@ feature 'Master deletes tag', feature: true do
     visit namespace_project_tags_path(project.namespace, project)
   end
 
-  context 'from the tags list page' do
+  context 'from the tags list page', js: true do
     scenario 'deletes the tag' do
       expect(page).to have_content 'v1.1.0'
 
-      page.within('.content') do
-        first('.btn-remove').click
-      end
+      delete_first_tag
 
-      expect(current_path).to eq(
-        namespace_project_tags_path(project.namespace, project))
       expect(page).not_to have_content 'v1.1.0'
     end
   end
@@ -37,4 +33,23 @@ feature 'Master deletes tag', feature: true do
       expect(page).not_to have_content 'v1.0.0'
     end
   end
+
+  context 'when pre-receive hook fails', js: true do
+    before do
+      allow_any_instance_of(GitHooksService).to receive(:execute)
+        .and_raise(GitHooksService::PreReceiveError, 'Do not delete tags')
+    end
+
+    scenario 'shows the error message' do
+      delete_first_tag
+
+      expect(page).to have_content('Do not delete tags')
+    end
+  end
+
+  def delete_first_tag
+    page.within('.content') do
+      first('.btn-remove').click
+    end
+  end
 end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 29d2c2447205635e9e8f0af6c013ca6b4b2092a6..555f84c477297245cd97d73d8489453d8953cc00 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -27,10 +27,20 @@ feature 'Master views tags', feature: true do
 
   context 'when project has tags' do
     let(:project) { create(:project, namespace: user.namespace) }
+    let(:repository) { project.repository }
+
     before do
       visit namespace_project_tags_path(project.namespace, project)
     end
 
+    scenario 'avoids a N+1 query in branches index' do
+      control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_tags_path(project.namespace, project) }.count
+
+      %w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') }
+
+      expect { visit namespace_project_tags_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+    end
+
     scenario 'views the tags list page' do
       expect(page).to have_content 'v1.0.0'
     end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
index fec28c55d30e80fad4e52867f79097c6429827bb..4d5bd476301d7b647f4ee51806e9e526ae139440 100644
--- a/spec/features/todos/todos_sorting_spec.rb
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -56,8 +56,8 @@ describe "Dashboard > User sorts todos", feature: true do
       expect(results_list.all('p')[4]).to have_content("merge_request_1")
     end
 
-    it "sorts by priority" do
-      click_link "Priority"
+    it "sorts by label priority" do
+      click_link "Label priority"
 
       results_list = page.find('.todos-list')
       expect(results_list.all('p')[0]).to have_content("issue_3")
@@ -85,8 +85,8 @@ describe "Dashboard > User sorts todos", feature: true do
       visit dashboard_todos_path
     end
 
-    it "doesn't mix issues and merge requests priorities" do
-      click_link "Priority"
+    it "doesn't mix issues and merge requests label priorities" do
+      click_link "Label priority"
 
       results_list = page.find('.todos-list')
       expect(results_list.all('p')[0]).to have_content("issue_1")
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3495091a0d54f8ebc6df647d8b638647f2f320a1..850020109d417b4f587085a9e1f185558f71cb53 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -31,14 +31,16 @@ describe 'Dashboard Todos', feature: true do
       end
 
       it 'shows due date as today' do
-        page.within first('.todo') do
+        within first('.todo') do
           expect(page).to have_content 'Due today'
         end
       end
 
       shared_examples 'deleting the todo' do
         before do
-          first('.js-done-todo').click
+          within first('.todo') do
+            click_link 'Done'
+          end
         end
 
         it 'is marked as done-reversible in the list' do
@@ -62,9 +64,11 @@ describe 'Dashboard Todos', feature: true do
 
       shared_examples 'deleting and restoring the todo' do
         before do
-          first('.js-done-todo').click
-          wait_for_ajax
-          first('.js-undo-todo').click
+          within first('.todo') do
+            click_link 'Done'
+            wait_for_ajax
+            click_link 'Undo'
+          end
         end
 
         it 'is marked back as pending in the list' do
@@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do
       end
     end
 
+    context 'User has done todos', js: true do
+      before do
+        create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
+        login_as(user)
+        visit dashboard_todos_path(state: :done)
+      end
+
+      it 'has the done todo present' do
+        expect(page).to have_selector('.todos-list .todo.todo-done', count: 1)
+      end
+
+      describe 'restoring the todo' do
+        before do
+          within first('.todo') do
+            click_link 'Add todo'
+          end
+        end
+
+        it 'is removed from the list' do
+          expect(page).not_to have_selector('.todos-list .todo.todo-done')
+        end
+
+        it 'updates todo count' do
+          expect(page).to have_content 'To do 1'
+          expect(page).to have_content 'Done 0'
+        end
+      end
+    end
+
     context 'User has Todos with labels spanning multiple projects' do
       before do
         label1 = create(:label, project: project)
@@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do
       describe 'mark all as done', js: true do
         before do
           visit dashboard_todos_path
-          click_link('Mark all as done')
+          click_link 'Mark all as done'
         end
 
         it 'shows "All done" message!' do
@@ -151,6 +184,60 @@ describe 'Dashboard Todos', feature: true do
           expect(page).to have_content "You're all done!"
           expect(page).not_to have_selector('.gl-pagination')
         end
+
+        it 'shows "Undo mark all as done" button' do
+          expect(page).to have_selector('.js-todos-mark-all', visible: false)
+          expect(page).to have_selector('.js-todos-undo-all', visible: true)
+        end
+      end
+
+      describe 'undo mark all as done', js: true do
+        before do
+          visit dashboard_todos_path
+        end
+
+        it 'shows the restored todo list' do
+          mark_all_and_undo
+
+          expect(page).to have_selector('.todos-list .todo', count: 1)
+          expect(page).to have_selector('.gl-pagination')
+          expect(page).not_to have_content "You're all done!"
+        end
+
+        it 'updates todo count' do
+          mark_all_and_undo
+
+          expect(page).to have_content 'To do 2'
+          expect(page).to have_content 'Done 0'
+        end
+
+        it 'shows "Mark all as done" button' do
+          mark_all_and_undo
+
+          expect(page).to have_selector('.js-todos-mark-all', visible: true)
+          expect(page).to have_selector('.js-todos-undo-all', visible: false)
+        end
+
+        context 'User has deleted a todo' do
+          before do
+            within first('.todo') do
+              click_link 'Done'
+            end
+          end
+
+          it 'shows the restored todo list with the deleted todo' do
+            mark_all_and_undo
+
+            expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1)
+          end
+        end
+
+        def mark_all_and_undo
+          click_link 'Mark all as done'
+          wait_for_ajax
+          click_link 'Undo mark all as done'
+          wait_for_ajax
+        end
       end
     end
 
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 4a7511589d6963d0bb288d7091307350ac398814..c1ae6db00c640e1209f9a4cb121ba2624831c25f 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,28 +1,175 @@
 require 'spec_helper'
 
-describe 'Triggers' do
+feature 'Triggers', feature: true, js: true do
+  let(:trigger_title) { 'trigger desc' }
   let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let(:guest_user) { create(:user) }
   before { login_as(user) }
 
   before do
-    @project = FactoryGirl.create :empty_project
+    @project = create(:empty_project)
     @project.team << [user, :master]
+    @project.team << [user2, :master]
+    @project.team << [guest_user, :guest]
     visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
   end
 
-  context 'create a trigger' do
-    before do
-      click_on 'Add trigger'
-      expect(@project.triggers.count).to eq(1)
+  describe 'create trigger workflow' do
+    scenario 'prevents adding new trigger with no description' do
+      fill_in 'trigger_description', with: ''
+      click_button 'Add trigger'
+
+      # See if input has error due to empty value
+      expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;'
+    end
+
+    scenario 'adds new trigger with description' do
+      fill_in 'trigger_description', with: 'trigger desc'
+      click_button 'Add trigger'
+
+      # See if "trigger creation successful" message displayed and description and owner are correct
+      expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
+      expect(page.find('.triggers-list')).to have_content 'trigger desc'
+      expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+    end
+  end
+
+  describe 'edit trigger workflow' do
+    let(:new_trigger_title) { 'new trigger' }
+
+    scenario 'click on edit trigger opens edit trigger page' do
+      create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if edit page has correct descrption
+      find('a[title="Edit"]').click
+      expect(page.find('#trigger_description').value).to have_content 'trigger desc'
+    end
+
+    scenario 'edit trigger and save' do
+      create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if edit page opens, then fill in new description and save
+      find('a[title="Edit"]').click
+      fill_in 'trigger_description', with: new_trigger_title
+      click_button 'Save trigger'
+
+      # See if "trigger updated successfully" message displayed and description and owner are correct
+      expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+      expect(page.find('.triggers-list')).to have_content new_trigger_title
+      expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+    end
+
+    scenario 'edit "legacy" trigger and save' do
+      # Create new trigger without owner association, i.e. Legacy trigger
+      create(:ci_trigger, owner: nil, project: @project)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if the trigger can be edited and description is blank
+      find('a[title="Edit"]').click
+      expect(page.find('#trigger_description').value).to have_content ''
+
+      # See if trigger can be updated with description and saved successfully
+      fill_in 'trigger_description', with: new_trigger_title
+      click_button 'Save trigger'
+      expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+      expect(page.find('.triggers-list')).to have_content new_trigger_title
+    end
+  end
+
+  describe 'trigger "Take ownership" workflow' do
+    before(:each) do
+      create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+    end
+
+    scenario 'button "Take ownership" has correct alert' do
+      expected_alert = 'By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?'
+      expect(page.find('a.btn-trigger-take-ownership')['data-confirm']).to eq expected_alert
     end
 
-    it 'contains trigger token' do
-      expect(page).to have_content(@project.triggers.first.token)
+    scenario 'take trigger ownership' do
+      # See if "Take ownership" on trigger works post trigger creation
+      find('a.btn-trigger-take-ownership').click
+      page.accept_confirm do
+        expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
+        expect(page.find('.triggers-list')).to have_content trigger_title
+        expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+      end
     end
+  end
+
+  describe 'trigger "Revoke" workflow' do
+    before(:each) do
+      create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+    end
+
+    scenario 'button "Revoke" has correct alert' do
+      expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
+      expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert
+    end
+
+    scenario 'revoke trigger' do
+      # See if "Revoke" on trigger works post trigger creation
+      find('a.btn-trigger-revoke').click
+      page.accept_confirm do
+        expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+        expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
+      end
+    end
+  end
+
+  describe 'show triggers workflow' do
+    scenario 'contains trigger description placeholder' do
+      expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
+    end
+
+    scenario 'show "legacy" badge for legacy trigger' do
+      create(:ci_trigger, owner: nil, project: @project)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable
+      expect(page.find('.triggers-list')).to have_content 'legacy'
+      expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
+    end
+
+    scenario 'show "invalid" badge for trigger with owner having insufficient permissions' do
+      create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable
+      expect(page.find('.triggers-list')).to have_content 'invalid'
+      expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
+    end
+
+    scenario 'do not show "Edit" or full token for not owned trigger' do
+      # Create trigger with user different from current_user
+      create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button
+      expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
+      expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard')
+
+      # See if trigger owner name doesn't match with current_user and trigger is non-editable
+      expect(page.find('.triggers-list .trigger-owner')).not_to have_content @user.name
+      expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
+    end
+
+    scenario 'show "Edit" and full token for owned trigger' do
+      create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+      visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+      # See if trigger shows full token and has copy-to-clipboard button
+      expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
+      expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard')
 
-    it 'revokes the trigger' do
-      click_on 'Revoke'
-      expect(@project.triggers.count).to eq(0)
+      # See if trigger owner name matches with current_user and is editable
+      expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+      expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
     end
   end
 end
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f88a515f7fcd0594a8c62b54203204bd108cf908
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to group', feature: true do
+  scenario 'they see the new avatar' do
+    user = create(:user)
+    group = create(:group)
+    group.add_owner(user)
+    login_as(user)
+
+    visit edit_group_path(group)
+    attach_file(
+      'group_avatar',
+      Rails.root.join('spec', 'fixtures', 'dk.png'),
+      visible: false
+    )
+
+    click_button 'Save group'
+
+    visit group_path(group)
+
+    expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"]))
+
+    # Cheating here to verify something that isn't user-facing, but is important
+    expect(group.reload.avatar.file).to exist
+  end
+end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0dfd29045e55242989758482d14a1dc001170635
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to profile', feature: true do
+  scenario 'they see their new avatar' do
+    user = create(:user)
+    login_as(user)
+
+    visit profile_path
+    attach_file(
+      'user_avatar',
+      Rails.root.join('spec', 'fixtures', 'dk.png'),
+      visible: false
+    )
+
+    click_button 'Update profile settings'
+
+    visit user_path(user)
+
+    expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"]))
+
+    # Cheating here to verify something that isn't user-facing, but is important
+    expect(user.reload.avatar.file).to exist
+  end
+end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c160dd74b40055f7db5d6181e9e68d1051bd2de
--- /dev/null
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+feature 'User uploads file to note', feature: true do
+  include DropzoneHelper
+
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
+
+  scenario 'they see the attached file', js: true do
+    issue = create(:issue, project: project, author: user)
+
+    login_as(user)
+    visit namespace_project_issue_path(project.namespace, project, issue)
+
+    dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
+    click_button 'Comment'
+    wait_for_ajax
+
+    expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+      .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+  end
+end
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..336c4092c98943a372ead0a880007b47e0a946b1
--- /dev/null
+++ b/spec/features/user_callout_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'User Callouts', js: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+
+  before do
+    login_as(user)
+    project.team << [user, :master]    
+  end
+
+  it 'takes you to the profile preferences when the link is clicked' do    
+    visit dashboard_projects_path
+    click_link 'Check it out'
+    expect(current_path).to eq profile_preferences_path
+  end
+
+  describe 'user callout should appear in two routes' do
+    it 'shows up on the user profile' do
+      visit user_path(user)
+      expect(find('.user-callout')).to have_content 'Customize your experience'
+    end
+
+    it 'shows up on the dashboard projects' do
+      visit dashboard_projects_path
+      expect(find('.user-callout')).to have_content 'Customize your experience'
+    end
+  end
+
+  it 'hides the user callout when click on the dismiss icon' do
+    visit user_path(user)
+    within('.user-callout') do
+      find('.close-user-callout').click
+    end
+    expect(page).not_to have_selector('#user-callout')
+  end
+end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..14564abb16d2b89b4411fe6e0c40353a5124b807
--- /dev/null
+++ b/spec/features/users/rss_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+feature 'User RSS' do
+  let(:path) { user_path(create(:user)) }
+
+  context 'when signed in' do
+    before do
+      login_as(create(:user))
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button with current_user's private token"
+  end
+
+  context 'when signed out' do
+    before do
+      visit path
+    end
+
+    it_behaves_like "it has an RSS button without a private token"
+  end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 2a00842747872fb0157712325dddb6dfc69dbdd0..ee52dc651756d2f4bc5e4b276f1daa40bbe1f5b7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -101,6 +101,41 @@ describe IssuesFinder do
         end
       end
 
+      context 'filtering by started milestone' do
+        let(:params) { { milestone_title: Milestone::Started.name } }
+
+        let(:project_no_started_milestones) { create(:empty_project, :public) }
+        let(:project_started_1_and_2) { create(:empty_project, :public) }
+        let(:project_started_8) { create(:empty_project, :public) }
+
+        let(:yesterday) { Date.today - 1.day }
+        let(:tomorrow) { Date.today + 1.day }
+        let(:two_days_ago) { Date.today - 2.days }
+
+        let(:milestones) do
+          [
+            create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
+            create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
+            create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
+            create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+            create(:milestone, project: project_started_8, title: '7.0'),
+            create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
+            create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
+          ]
+        end
+
+        before do
+          milestones.each do |milestone|
+            create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+          end
+        end
+
+        it 'returns issues in the started milestones for each project' do
+          expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0')
+          expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday)
+        end
+      end
+
       context 'filtering by label' do
         let(:params) { { label_name: label.title } }
 
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf691cf684b72351ca00be301c1001cfa0c8fefe
--- /dev/null
+++ b/spec/finders/members_finder_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe MembersFinder, '#execute' do
+  let(:group)        { create(:group) }
+  let(:nested_group) { create(:group, :access_requestable, parent: group) }
+  let(:project)      { create(:project, namespace: nested_group) }
+  let(:user1)        { create(:user) }
+  let(:user2)        { create(:user) }
+  let(:user3)        { create(:user) }
+  let(:user4)        { create(:user) }
+
+  it 'returns members for project and parent groups' do
+    nested_group.request_access(user1)
+    member1 = group.add_master(user2)
+    member2 = nested_group.add_master(user3)
+    member3 = project.add_master(user4)
+
+    result = described_class.new(project, user2).execute
+
+    expect(result.to_a).to eq([member3, member2, member1])
+  end
+end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index f8b05d4e9bc197c6e85f97f767eb90c006177887..77a04507be105896e24737c6f8ad435584258697 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -111,7 +111,7 @@ describe NotesFinder do
       end
 
       it 'raises an exception for an invalid target_type' do
-        params.merge!(target_type: 'invalid')
+        params[:target_type] = 'invalid'
         expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
       end
 
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd92664ca24c6b691e43002297b57b996f05e441
--- /dev/null
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe PersonalAccessTokensFinder do
+  def finder(options = {})
+    described_class.new(options)
+  end
+
+  describe '#execute' do
+    let(:user) { create(:user) }
+    let(:params) { {} }
+    let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+    let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
+    let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+    let!(:active_impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+    let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) }
+    let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) }
+
+    subject { finder(params).execute }
+
+    describe 'without user' do
+      it do
+        is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
+          revoked_personal_access_token, expired_personal_access_token,
+          revoked_impersonation_token, expired_impersonation_token)
+      end
+
+      describe 'without impersonation' do
+        before { params[:impersonation] = false }
+
+        it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) }
+
+        describe 'with active state' do
+          before { params[:state] = 'active' }
+
+          it { is_expected.to contain_exactly(active_personal_access_token) }
+        end
+
+        describe 'with inactive state' do
+          before { params[:state] = 'inactive' }
+
+          it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
+        end
+      end
+
+      describe 'with impersonation' do
+        before { params[:impersonation] = true }
+
+        it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
+
+        describe 'with active state' do
+          before { params[:state] = 'active' }
+
+          it { is_expected.to contain_exactly(active_impersonation_token) }
+        end
+
+        describe 'with inactive state' do
+          before { params[:state] = 'inactive' }
+
+          it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+        end
+      end
+
+      describe 'with active state' do
+        before { params[:state] = 'active' }
+
+        it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) }
+      end
+
+      describe 'with inactive state' do
+        before { params[:state] = 'inactive' }
+
+        it do
+          is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
+            expired_impersonation_token, revoked_impersonation_token)
+        end
+      end
+
+      describe 'with id' do
+        subject { finder(params).find_by(id: active_personal_access_token.id) }
+
+        it { is_expected.to eq(active_personal_access_token) }
+
+        describe 'with impersonation' do
+          before { params[:impersonation] = true }
+
+          it { is_expected.to be_nil }
+        end
+      end
+
+      describe 'with token' do
+        subject { finder(params).find_by(token: active_personal_access_token.token) }
+
+        it { is_expected.to eq(active_personal_access_token) }
+
+        describe 'with impersonation' do
+          before { params[:impersonation] = true }
+
+          it { is_expected.to be_nil }
+        end
+      end
+    end
+
+    describe 'with user' do
+      let(:user2) { create(:user) }
+      let!(:other_user_active_personal_access_token) { create(:personal_access_token, user: user2) }
+      let!(:other_user_expired_personal_access_token) { create(:personal_access_token, :expired, user: user2) }
+      let!(:other_user_revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user2) }
+      let!(:other_user_active_impersonation_token) { create(:personal_access_token, :impersonation, user: user2) }
+      let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) }
+      let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) }
+
+      before { params[:user] = user }
+
+      it do
+        is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
+          revoked_personal_access_token, expired_personal_access_token,
+          revoked_impersonation_token, expired_impersonation_token)
+      end
+
+      describe 'without impersonation' do
+        before { params[:impersonation] = false }
+
+        it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) }
+
+        describe 'with active state' do
+          before { params[:state] = 'active' }
+
+          it { is_expected.to contain_exactly(active_personal_access_token) }
+        end
+
+        describe 'with inactive state' do
+          before { params[:state] = 'inactive' }
+
+          it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
+        end
+      end
+
+      describe 'with impersonation' do
+        before { params[:impersonation] = true }
+
+        it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
+
+        describe 'with active state' do
+          before { params[:state] = 'active' }
+
+          it { is_expected.to contain_exactly(active_impersonation_token) }
+        end
+
+        describe 'with inactive state' do
+          before { params[:state] = 'inactive' }
+
+          it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+        end
+      end
+
+      describe 'with active state' do
+        before { params[:state] = 'active' }
+
+        it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) }
+      end
+
+      describe 'with inactive state' do
+        before { params[:state] = 'inactive' }
+
+        it do
+          is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
+            expired_impersonation_token, revoked_impersonation_token)
+        end
+      end
+
+      describe 'with id' do
+        subject { finder(params).find_by(id: active_personal_access_token.id) }
+
+        it { is_expected.to eq(active_personal_access_token) }
+
+        describe 'with impersonation' do
+          before { params[:impersonation] = true }
+
+          it { is_expected.to be_nil }
+        end
+      end
+
+      describe 'with token' do
+        subject { finder(params).find_by(token: active_personal_access_token.token) }
+
+        it { is_expected.to eq(active_personal_access_token) }
+
+        describe 'with impersonation' do
+          before { params[:impersonation] = true }
+
+          it { is_expected.to be_nil }
+        end
+      end
+    end
+  end
+end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index fdc8215aa47114d3160ffe0303a4b86eaab9e98e..6bada7b3eb9610ac82dcfa30fb3c2dca822c42ba 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -39,8 +39,8 @@ describe PipelinesFinder do
       end
     end
 
-    # Scoping to running will speed up the test as it doesn't hit the FS
-    let(:params) { { scope: 'running' } }
+    # Scoping to pending will speed up the test as it doesn't hit the FS
+    let(:params) { { scope: 'pending' } }
 
     it 'orders in descending order on ID' do
       feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 8e19cee54404cb2fd28b9cc3b15fd77f60c7f93a..21c078e0f44ec0e1de82e260f6412180c5ba9a76 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -11,6 +11,7 @@
     "title": { "type": "string" },
     "confidential": { "type": "boolean" },
     "due_date": { "type": ["date", "null"] },
+    "relative_position": { "type": "integer" },
     "labels": {
       "type": "array",
       "items": {
diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json
new file mode 100644
index 0000000000000000000000000000000000000000..f2ee9c925ae2450b36519a8327212a8c76af238f
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v3/issues.json
@@ -0,0 +1,77 @@
+{
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties" : {
+      "id": { "type": "integer" },
+      "iid": { "type": "integer" },
+      "project_id": { "type": "integer" },
+      "title": { "type": "string" },
+      "description": { "type": ["string", "null"] },
+      "state": { "type": "string" },
+      "created_at": { "type": "date" },
+      "updated_at": { "type": "date" },
+      "labels": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "milestone": {
+        "type": "object",
+        "properties": {
+          "id": { "type": "integer" },
+          "iid": { "type": "integer" },
+          "project_id": { "type": "integer" },
+          "title": { "type": "string" },
+          "description": { "type": ["string", "null"] },
+          "state": { "type": "string" },
+          "created_at": { "type": "date" },
+          "updated_at": { "type": "date" },
+          "due_date": { "type": "date" },
+          "start_date": { "type": "date" }
+        },
+        "additionalProperties": false
+      },
+      "assignee": {
+        "type": ["object", "null"],
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "author": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "user_notes_count": { "type": "integer" },
+      "upvotes": { "type": "integer" },
+      "downvotes": { "type": "integer" },
+      "due_date": { "type": ["date", "null"] },
+      "confidential": { "type": "boolean" },
+      "web_url": { "type": "uri" },
+      "subscribed": { "type": ["boolean"] }
+    },
+    "required": [
+      "id", "iid", "project_id", "title", "description",
+      "state", "created_at", "updated_at", "labels",
+      "milestone", "assignee", "author", "user_notes_count",
+      "upvotes", "downvotes", "due_date", "confidential",
+      "web_url", "subscribed"
+    ],
+    "additionalProperties": false
+  }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
new file mode 100644
index 0000000000000000000000000000000000000000..01f9fbb2c894ac9037cb4d47200ede932e7c94ef
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
@@ -0,0 +1,89 @@
+{
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties" : {
+      "id": { "type": "integer" },
+      "iid": { "type": "integer" },
+      "project_id": { "type": "integer" },
+      "title": { "type": "string" },
+      "description": { "type": ["string", "null"] },
+      "state": { "type": "string" },
+      "created_at": { "type": "date" },
+      "updated_at": { "type": "date" },
+      "target_branch": { "type": "string" },
+      "source_branch": { "type": "string" },
+      "upvotes": { "type": "integer" },
+      "downvotes": { "type": "integer" },
+      "author": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "assignee": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "source_project_id": { "type": "integer" },
+      "target_project_id": { "type": "integer" },
+      "labels": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "work_in_progress": { "type": "boolean" },
+      "milestone": {
+        "type": ["object", "null"],
+        "properties": {
+          "id": { "type": "integer" },
+          "iid": { "type": "integer" },
+          "project_id": { "type": "integer" },
+          "title": { "type": "string" },
+          "description": { "type": ["string", "null"] },
+          "state": { "type": "string" },
+          "created_at": { "type": "date" },
+          "updated_at": { "type": "date" },
+          "due_date": { "type": "date" },
+          "start_date": { "type": "date" }
+        },
+        "additionalProperties": false
+      },
+      "merge_when_build_succeeds": { "type": "boolean" },
+      "merge_status": { "type": "string" },
+      "sha": { "type": "string" },
+      "merge_commit_sha": { "type": ["string", "null"] },
+      "user_notes_count": { "type": "integer" },
+      "should_remove_source_branch": { "type": ["boolean", "null"] },
+      "force_remove_source_branch": { "type": ["boolean", "null"] },
+      "web_url": { "type": "uri" },
+      "subscribed": { "type": ["boolean"] }
+    },
+    "required": [
+      "id", "iid", "project_id", "title", "description",
+      "state", "created_at", "updated_at", "target_branch",
+      "source_branch", "upvotes", "downvotes", "author",
+      "assignee", "source_project_id", "target_project_id",
+      "labels", "work_in_progress", "milestone", "merge_when_build_succeeds",
+      "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+      "should_remove_source_branch", "force_remove_source_branch",
+      "web_url", "subscribed"
+    ],
+    "additionalProperties": false
+  }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
new file mode 100644
index 0000000000000000000000000000000000000000..52199e757342076ba8ab3cfbd12615fd21a7b9fb
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -0,0 +1,76 @@
+{
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties" : {
+      "id": { "type": "integer" },
+      "iid": { "type": "integer" },
+      "project_id": { "type": "integer" },
+      "title": { "type": "string" },
+      "description": { "type": ["string", "null"] },
+      "state": { "type": "string" },
+      "created_at": { "type": "date" },
+      "updated_at": { "type": "date" },
+      "labels": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "milestone": {
+        "type": "object",
+        "properties": {
+          "id": { "type": "integer" },
+          "iid": { "type": "integer" },
+          "project_id": { "type": "integer" },
+          "title": { "type": "string" },
+          "description": { "type": ["string", "null"] },
+          "state": { "type": "string" },
+          "created_at": { "type": "date" },
+          "updated_at": { "type": "date" },
+          "due_date": { "type": "date" },
+          "start_date": { "type": "date" }
+        },
+        "additionalProperties": false
+      },
+      "assignee": {
+        "type": ["object", "null"],
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "author": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "user_notes_count": { "type": "integer" },
+      "upvotes": { "type": "integer" },
+      "downvotes": { "type": "integer" },
+      "due_date": { "type": ["date", "null"] },
+      "confidential": { "type": "boolean" },
+      "web_url": { "type": "uri" }
+    },
+    "required": [
+      "id", "iid", "project_id", "title", "description",
+      "state", "created_at", "updated_at", "labels",
+      "milestone", "assignee", "author", "user_notes_count",
+      "upvotes", "downvotes", "due_date", "confidential",
+      "web_url"
+    ],
+    "additionalProperties": false
+  }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
new file mode 100644
index 0000000000000000000000000000000000000000..51642e8cbb8dc9bc9a9b85662e766f0df498342b
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -0,0 +1,88 @@
+{
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties" : {
+      "id": { "type": "integer" },
+      "iid": { "type": "integer" },
+      "project_id": { "type": "integer" },
+      "title": { "type": "string" },
+      "description": { "type": ["string", "null"] },
+      "state": { "type": "string" },
+      "created_at": { "type": "date" },
+      "updated_at": { "type": "date" },
+      "target_branch": { "type": "string" },
+      "source_branch": { "type": "string" },
+      "upvotes": { "type": "integer" },
+      "downvotes": { "type": "integer" },
+      "author": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "assignee": {
+        "type": "object",
+        "properties": {
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "id": { "type": "integer" },
+          "state": { "type": "string" },
+          "avatar_url": { "type": "uri" },
+          "web_url": { "type": "uri" }
+        },
+        "additionalProperties": false
+      },
+      "source_project_id": { "type": "integer" },
+      "target_project_id": { "type": "integer" },
+      "labels": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "work_in_progress": { "type": "boolean" },
+      "milestone": {
+        "type": ["object", "null"],
+        "properties": {
+          "id": { "type": "integer" },
+          "iid": { "type": "integer" },
+          "project_id": { "type": "integer" },
+          "title": { "type": "string" },
+          "description": { "type": ["string", "null"] },
+          "state": { "type": "string" },
+          "created_at": { "type": "date" },
+          "updated_at": { "type": "date" },
+          "due_date": { "type": "date" },
+          "start_date": { "type": "date" }
+        },
+        "additionalProperties": false
+      },
+      "merge_when_pipeline_succeeds": { "type": "boolean" },
+      "merge_status": { "type": "string" },
+      "sha": { "type": "string" },
+      "merge_commit_sha": { "type": ["string", "null"] },
+      "user_notes_count": { "type": "integer" },
+      "should_remove_source_branch": { "type": ["boolean", "null"] },
+      "force_remove_source_branch": { "type": ["boolean", "null"] },
+      "web_url": { "type": "uri" }
+    },
+    "required": [
+      "id", "iid", "project_id", "title", "description",
+      "state", "created_at", "updated_at", "target_branch",
+      "source_branch", "upvotes", "downvotes", "author",
+      "assignee", "source_project_id", "target_project_id",
+      "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
+      "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+      "should_remove_source_branch", "force_remove_source_branch",
+      "web_url"
+    ],
+    "additionalProperties": false
+  }
+}
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
similarity index 100%
rename from spec/fixtures/api/schemas/user/login.json
rename to spec/fixtures/api/schemas/public_api/v4/user/login.json
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
similarity index 100%
rename from spec/fixtures/api/schemas/user/public.json
rename to spec/fixtures/api/schemas/public_api/v4/user/public.json
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index f3e7c2d1a9f2ed31ec12af540249360baa8c2fb3..0cdbc32431d9246ce8f6f6e3e132b54742c0e0d8 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -79,6 +79,11 @@ As permissive as it is, we've allowed even more stuff:
 
 <span>span tag</span>
 
+<details>
+<summary>Summary lines are collapsible:</summary>
+Hiding the details until expanded.
+</details>
+
 <a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a>
 
 <a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a>
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index fd40fe999411f3e4a862f20e673f0f5d3cf58a9a..5c07ea8a872cfaa1a01ee34ae08bbf470a103871 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -58,7 +58,7 @@ describe ApplicationHelper do
       project = create(:empty_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).
+      expect(helper.project_icon(project.full_path).to_s).
         to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
     end
 
@@ -68,7 +68,7 @@ describe ApplicationHelper do
       allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
 
       avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
-      expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match(
+      expect(helper.project_icon(project.full_path).to_s).to match(
         image_tag(avatar_url))
     end
   end
@@ -193,8 +193,8 @@ describe ApplicationHelper do
   describe 'time_ago_with_tooltip' do
     def element(*arguments)
       Time.zone = 'UTC'
-      time = Time.zone.parse('2015-07-02 08:23')
-      element = helper.time_ago_with_tooltip(time, *arguments)
+      @time = Time.zone.parse('2015-07-02 08:23')
+      element = helper.time_ago_with_tooltip(@time, *arguments)
 
       Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
     end
@@ -204,7 +204,7 @@ describe ApplicationHelper do
     end
 
     it 'includes the date string' do
-      expect(element.text).to eq '2015-07-02 08:23:00 UTC'
+      expect(element.text).to eq @time.strftime("%b %d, %Y")
     end
 
     it 'has a datetime attribute' do
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 49ea4fa6d3e18c1c508f831ed8b1a40a813a54d4..cd3281d6f5183893549dd80afab6ee251dc06c37 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -55,7 +55,7 @@ describe AuthHelper do
     context 'all the button based providers are disabled via application_setting' do
       it 'returns false' do
         stub_application_setting(
-          disabled_oauth_sign_in_sources: ['github', 'twitter']
+          disabled_oauth_sign_in_sources: %w(github twitter)
         )
 
         expect(helper.button_based_providers_enabled?).to be false
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index fa516f9903ecc9a83c1730959d4a49394446b01b..bead79484867629accca4414b7bb1ed4f2afa26b 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -19,12 +19,12 @@ describe BlobHelper do
   describe '#highlight' do
     it 'returns plaintext for unknown lexer context' do
       result = helper.highlight(blob_name, no_context_content)
-      expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
+      expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
     end
 
     it 'highlights single block' do
-      expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
+      expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
 
       expect(helper.highlight(blob_name, blob_content)).to eq(expected)
     end
@@ -43,10 +43,10 @@ describe BlobHelper do
       let(:blob_name) { 'test.diff' }
       let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
       let(:expected) do
-        %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
-<span id="LC2" class="line"><span class="gi">+bbb</span></span>
-<span id="LC3" class="line"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line"> ddd</span></code></pre>)
+        %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
       end
 
       it 'highlights each line properly' do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 637b02d938877df3b8d3df37b5d5d3a107522afe..174cc84a97b2f673a2fb70ae22829bee22f7a3c4 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -16,4 +16,11 @@ describe CiStatusHelper do
       helper.ci_icon_for_status(failed_commit.status)
     end
   end
+
+  describe "#pipeline_status_cache_key" do
+    it "builds a cache key for pipeline status" do
+      pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success")
+      expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
+    end
+  end
 end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 3223556e1d3444980ed81a24d912062f3f5f7bf1..cd112dbb2fbf94a14f5098bb9bc9f034697f265d 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -43,4 +43,36 @@ describe EmailsHelper do
       end
     end
   end
+
+  describe '#header_logo' do
+    context 'there is a brand item with a logo' do
+      it 'returns the brand header logo' do
+        appearance = create :appearance, header_logo: fixture_file_upload(
+          Rails.root.join('spec/fixtures/dk.png')
+        )
+
+        expect(header_logo).to eq(
+          %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
+        )
+      end
+    end
+
+    context 'there is a brand item without a logo' do
+      it 'returns the default header logo' do
+        create :appearance, header_logo: nil
+
+        expect(header_logo).to eq(
+          %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+        )
+      end
+    end
+
+    context 'there is no brand item' do
+      it 'returns the default header logo' do
+        expect(header_logo).to eq(
+          %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+        )
+      end
+    end
+  end
 end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 594b40303bc50793ec1632de1960d4529e1a748c..70443d27f339b38997149551e3998701550cbc7f 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -28,7 +28,7 @@ describe EventsHelper do
 
     it 'displays the first line of a code block' do
       input = "```\nCode block\nwith two lines\n```"
-      expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
+      expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
 
       expect(helper.event_note(input)).to match(expected)
     end
@@ -55,10 +55,15 @@ describe EventsHelper do
     it 'preserves code color scheme' do
       input = "```ruby\ndef test\n  'hello world'\nend\n```"
       expected = '<pre class="code highlight js-syntax-highlight ruby">' \
-        "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
-        "  <span class=\"s1\">\'hello world\'</span>\n" \
-        "<span class=\"k\">end</span>\n" \
-        '</code></pre>'
+        "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
+        "</code></pre>"
+      expect(helper.event_note(input)).to eq(expected)
+    end
+
+    it 'preserves style attribute within a tag' do
+      input = '<span class="" style="background-color: #44ad8e; color: #FFFFFF;"></span>'
+      expected = '<p><span style="background-color: #44ad8e; color: #FFFFFF;"></span></p>'
+
       expect(helper.event_note(input)).to eq(expected)
     end
   end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index b8ec3521edb9892e9f0a56baa0c4a0ff9ac3402f..6cf3f86680a4f17d771772ed1fd97d1bcabe8c9c 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
     it 'replaces commit message with emoji to link' do
       actual = link_to_gfm(':book:Book', '/foo')
       expect(actual).
-        to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+        to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
     end
   end
 
@@ -152,9 +152,8 @@ describe GitlabMarkdownHelper do
   end
 
   describe '#first_line_in_markdown' do
-    let(:text) { "@#{user.username}, can you look at this?\nHello world\n"}
-
     it 'truncates Markdown properly' do
+      text = "@#{user.username}, can you look at this?\nHello world\n"
       actual = first_line_in_markdown(text, 100, project: project)
 
       doc = Nokogiri::HTML.parse(actual)
@@ -169,6 +168,23 @@ describe GitlabMarkdownHelper do
 
       expect(doc.content).to eq "@#{user.username}, can you look at this?..."
     end
+
+    it 'truncates Markdown with emoji properly' do
+      text = "foo :wink:\nbar :grinning:"
+      actual = first_line_in_markdown(text, 100, project: project)
+
+      doc = Nokogiri::HTML.parse(actual)
+
+      # Make sure we didn't create invalid markup
+      # But also account for the 2 errors caused by the unknown `gl-emoji` elements
+      expect(doc.errors.length).to eq(2)
+
+      expect(doc.css('gl-emoji').length).to eq(2)
+      expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
+      expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+
+      expect(doc.content).to eq "foo 😉\nbar 😀"
+    end
   end
 
   describe '#cross_project_reference' do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index df71680e44ce3f5b9b07172dcd42bfc9f43a7c9a..93bb711f29ab3a117abecbfd61e1871cc6e3e5f3 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -51,7 +51,7 @@ describe IssuablesHelper do
           utf8: '✓',
           author_id: '11',
           assignee_id: '18',
-          label_name: ['bug', 'discussion', 'documentation'],
+          label_name: %w(bug discussion documentation),
           milestone_title: 'v4.0',
           sort: 'due_date_asc',
           namespace_id: 'gitlab-org',
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 13fb9c1f1a7eacf7b47e3c394b66e3e49c6bf20a..f0554cc068d135aadc432fbace065645c77f1d31 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -55,8 +55,8 @@ describe IssuesHelper do
   describe "merge_requests_sentence" do
     subject { merge_requests_sentence(merge_requests)}
     let(:merge_requests) do
-      [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
-        build(:merge_request, iid: 3)]
+      [build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+       build(:merge_request, iid: 3)]
     end
 
     it { is_expected.to eq("!1, !2, or !3") }
@@ -113,7 +113,7 @@ describe IssuesHelper do
   describe "awards_sort" do
     it "sorts a hash so thumbsup and thumbsdown are always on top" do
       data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
-      expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
+      expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
     end
   end
 
@@ -131,4 +131,36 @@ describe IssuesHelper do
       expect(options).to have_selector('option', text: milestone2.title)
     end
   end
+
+  describe "#link_to_discussions_to_resolve" do
+    describe "passing only a merge request" do
+      let(:merge_request) { create(:merge_request) }
+
+      it "links just the merge request" do
+        expected_path = namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+
+        expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path)
+      end
+
+      it "containst the reference to the merge request" do
+        expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference)
+      end
+    end
+
+    describe "when passing a discussion" do
+      let(:diff_note) {  create(:diff_note_on_merge_request) }
+      let(:merge_request) { diff_note.noteable }
+      let(:discussion) { Discussion.new([diff_note]) }
+
+      it "links to the merge request with first note if a single discussion was passed" do
+        expected_path = Gitlab::UrlBuilder.build(diff_note)
+
+        expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path)
+      end
+
+      it "contains both the reference to the merge request and a mention of the discussion" do
+        expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})")
+      end
+    end
+  end
 end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 14a95479339e936a6942ef448a737514f6a5e00a..3cb809d42b52f357ecca23e86336e2522cd2f213 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -17,7 +17,7 @@ describe MilestonesHelper do
     it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") }
     it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") }
     it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") }
-    it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") }
+    it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") }
   end
 
   describe '#milestone_counts' do
@@ -47,4 +47,58 @@ describe MilestonesHelper do
       end
     end
   end
+
+  describe '#milestone_remaining_days' do
+    around do |example|
+      Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+    end
+
+    context 'when less than 31 days remaining' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+      it 'returns days remaining' do
+        expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+      end
+    end
+
+    context 'when less than 1 year and more than 30 days remaining' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+      it 'returns months remaining' do
+        expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+      end
+    end
+
+    context 'when more than 1 year remaining' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+      it 'returns years remaining' do
+        expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+      end
+    end
+
+    context 'when milestone is expired' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+      it 'returns "Past due"' do
+        expect(milestone_remaining).to eq("<strong>Past due</strong>")
+      end
+    end
+
+    context 'when milestone has start_date in the future' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+      it 'returns "Upcoming"' do
+        expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+      end
+    end
+
+    context 'when milestone has start_date in the past' do
+      let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+      it 'returns days elapsed' do
+        expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+      end
+    end
+  end
 end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index aca0bb1d794ced4166b728ea52ff1583f960cd63..fc6ad6419ac0aa04a3b63e620f30b780d11e08b3 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,6 +63,46 @@ describe ProjectsHelper do
     end
   end
 
+  describe "#project_list_cache_key" do
+    let(:project) { create(:project) }
+
+    it "includes the namespace" do
+      expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key)
+    end
+
+    it "includes the project" do
+      expect(helper.project_list_cache_key(project)).to include(project.cache_key)
+    end
+
+    it "includes the controller name" do
+      expect(helper.controller).to receive(:controller_name).and_return("testcontroller")
+
+      expect(helper.project_list_cache_key(project)).to include("testcontroller")
+    end
+
+    it "includes the controller action" do
+      expect(helper.controller).to receive(:action_name).and_return("testaction")
+
+      expect(helper.project_list_cache_key(project)).to include("testaction")
+    end
+
+    it "includes the application settings" do
+      settings = Gitlab::CurrentSettings.current_application_settings
+
+      expect(helper.project_list_cache_key(project)).to include(settings.cache_key)
+    end
+
+    it "includes a version" do
+      expect(helper.project_list_cache_key(project)).to include("v2.3")
+    end
+
+    it "includes the pipeline status when there is a status" do
+      create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+
+      expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success")
+    end
+  end
+
   describe 'link_to_member' do
     let(:group)   { create(:group) }
     let(:project) { create(:empty_project, group: group) }
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f3f174f3d149eda641eaac5f1a6dbbf51d3053a5
--- /dev/null
+++ b/spec/helpers/rss_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe RssHelper do
+  describe '#rss_url_options' do
+    context 'when signed in' do
+      it "includes the current_user's private_token" do
+        current_user = create(:user)
+        allow(helper).to receive(:current_user).and_return(current_user)
+        expect(helper.rss_url_options).to include private_token: current_user.private_token
+      end
+    end
+
+    context 'when signed out' do
+      it "does not have a private_token" do
+        allow(helper).to receive(:current_user).and_return(nil)
+        expect(helper.rss_url_options[:private_token]).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 4da1569e59f0dbe25b0b7dffae5adc3ec740f73a..28b8def331d63eeebf7417b2c7bbb48e754f294a 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -20,97 +20,97 @@ describe SubmoduleHelper do
       it 'detects ssh on standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
-        stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
-        expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+        stub_url([config.user, '@', config.host, ':gitlab-org/gitlab-ce.git'].join(''))
+        expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
       end
 
       it 'detects ssh on non-standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
-        stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
-        expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+        stub_url(['ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git'].join(''))
+        expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
       end
 
       it 'detects http on standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
-        stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
-        expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+        stub_url(['http://', config.host, '/gitlab-org/gitlab-ce.git'].join(''))
+        expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
       end
 
       it 'detects http on non-standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
-        stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
-        expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+        stub_url(['http://', config.host, ':3000/gitlab-org/gitlab-ce.git'].join(''))
+        expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
       end
 
       it 'works with relative_url_root' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
         allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
-        stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join(''))
-        expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+        stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join(''))
+        expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
       end
     end
 
     context 'submodule on github.com' do
       it 'detects ssh' do
         stub_url('git@github.com:gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'detects http' do
         stub_url('http://github.com/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'detects https' do
         stub_url('https://github.com/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'returns original with non-standard url' do
         stub_url('http://github.com/gitlab-org/gitlab-ce')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
 
         stub_url('http://github.com/another/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
       end
     end
 
     context 'submodule on gitlab.com' do
       it 'detects ssh' do
         stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'detects http' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'detects https' do
         stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+        expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
       end
 
       it 'returns original with non-standard url' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
 
         stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
       end
     end
 
     context 'submodule on unsupported' do
       it 'returns original' do
         stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
 
         stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git')
-        expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+        expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
       end
     end
 
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..21e0e74e0083ec2af186173093eb41a604723b42
--- /dev/null
+++ b/spec/helpers/todos_helper_spec.rb
@@ -0,0 +1,57 @@
+require "spec_helper"
+
+describe TodosHelper do
+  include GitlabRoutingHelper
+
+  describe '#todo_target_path' do
+    let(:project)       { create(:project) }
+    let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+    let(:issue)         { create(:issue, project: project) }
+    let(:note)          { create(:note_on_issue, noteable: issue, project: project) }
+
+    let(:mr_todo)           { build(:todo, project: project, target: merge_request) }
+    let(:issue_todo)        { build(:todo, project: project, target: issue) }
+    let(:note_todo)         { build(:todo, project: project, target: issue, note: note) }
+    let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) }
+
+    it 'returns correct path to the todo MR' do
+      expect(todo_target_path(mr_todo)).
+        to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}")
+    end
+
+    it 'returns correct path to the todo issue' do
+      expect(todo_target_path(issue_todo)).
+        to eq("/#{project.full_path}/issues/#{issue.iid}")
+    end
+
+    it 'returns correct path to the todo note' do
+      expect(todo_target_path(note_todo)).
+        to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}")
+    end
+
+    it 'returns correct path to build_todo MR when pipeline failed' do
+      expect(todo_target_path(build_failed_todo)).
+        to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines")
+    end
+  end
+
+  describe '#todo_projects_options' do
+    let(:projects) { create_list(:empty_project, 3) }
+    let(:user)     { create(:user) }
+
+    it 'returns users authorised projects in json format' do
+      projects.first.add_developer(user)
+      projects.second.add_developer(user)
+
+      allow(helper).to receive(:current_user).and_return(user)
+
+      expected_results = [
+        { 'id' => '', 'text' => 'Any Project' },
+        { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace },
+        { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace }
+      ]
+
+      expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
+    end
+  end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..889fe441171507be775c10a6db3191059828c54c
--- /dev/null
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe VersionCheckHelper do
+  describe '#version_status_badge' do
+    it 'should return nil if not dev environment and not enabled' do
+      allow(Rails.env).to receive(:production?) { false }
+      allow(current_application_settings).to receive(:version_check_enabled) { false }
+
+      expect(helper.version_status_badge).to be(nil)
+    end
+
+    context 'when production and enabled' do
+      before do
+        allow(Rails.env).to receive(:production?) { true }
+        allow(current_application_settings).to receive(:version_check_enabled) { true }
+        allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
+
+        @image_tag = helper.version_status_badge
+      end
+
+      it 'should return an image tag' do
+        expect(@image_tag).to match(/^<img/)
+      end
+
+      it 'should have a js prefixed css class' do
+        expect(@image_tag).to match(/class="js-version-status-badge"/)
+      end
+
+      it 'should have a VersionCheck url as the src' do
+        expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/)
+      end
+    end
+  end
+end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index baab30f482f6554efed482fd6cca8b4f598adc60..374517fec3743cab14d821acc3085f38d0fed9f9 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -12,43 +12,77 @@ describe '6_validations', lib: true do
     FileUtils.rm_rf('tmp/tests/paths')
   end
 
-  context 'with correct settings' do
-    before do
-      mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d')
+  describe 'validate_storages_config' do
+    context 'with correct settings' do
+      before do
+        mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+      end
+
+      it 'passes through' do
+        expect { validate_storages_config }.not_to raise_error
+      end
     end
 
-    it 'passes through' do
-      expect { validate_storages }.not_to raise_error
+    context 'with invalid storage names' do
+      before do
+        mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
+      end
+
+      it 'throws an error' do
+        expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+      end
     end
-  end
 
-  context 'with invalid storage names' do
-    before do
-      mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c')
+    context 'with incomplete settings' do
+      before do
+        mock_storages('foo' => {})
+      end
+
+      it 'throws an error suggesting the user to update its settings' do
+        expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
+      end
     end
 
-    it 'throws an error' do
-      expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+    context 'with deprecated settings structure' do
+      before do
+        mock_storages('foo' => 'tmp/tests/paths/a/b/c')
+      end
+
+      it 'throws an error suggesting the user to update its settings' do
+        expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n  path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
+      end
     end
   end
 
-  context 'with nested storage paths' do
-    before do
-      mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d')
-    end
+  describe 'validate_storages_paths' do
+    context 'with correct settings' do
+      before do
+        mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+      end
 
-    it 'throws an error' do
-      expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+      it 'passes through' do
+        expect { validate_storages_paths }.not_to raise_error
+      end
     end
-  end
 
-  context 'with similar but un-nested storage paths' do
-    before do
-      mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2')
+    context 'with nested storage paths' do
+      before do
+        mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
+      end
+
+      it 'throws an error' do
+        expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+      end
     end
 
-    it 'passes through' do
-      expect { validate_storages }.not_to raise_error
+    context 'with similar but un-nested storage paths' do
+      before do
+        mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
+      end
+
+      it 'passes through' do
+        expect { validate_storages_paths }.not_to raise_error
+      end
     end
   end
 
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
similarity index 87%
rename from spec/initializers/metrics_spec.rb
rename to spec/initializers/8_metrics_spec.rb
index bb59516237007bd5ab2a746cfb813c7be29786fc..570754621f397b75e64d06332ed27858cbef9934 100644
--- a/spec/initializers/metrics_spec.rb
+++ b/spec/initializers/8_metrics_spec.rb
@@ -1,5 +1,5 @@
 require 'spec_helper'
-require_relative '../../config/initializers/metrics'
+require_relative '../../config/initializers/8_metrics'
 
 describe 'instrument_classes', lib: true do
   let(:config) { double(:config) }
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74bdbb01166efc11265157755c95cab3b0e4ea82
--- /dev/null
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+require_relative '../../config/initializers/doorkeeper'
+
+describe Doorkeeper.configuration do
+  describe '#default_scopes' do
+    it 'matches Gitlab::Auth::DEFAULT_SCOPES' do
+      expect(subject.default_scopes).to eq Gitlab::Auth::DEFAULT_SCOPES
+    end
+  end
+
+  describe '#optional_scopes' do
+    it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
+      expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
+    end
+  end
+
+  describe '#resource_owner_authenticator' do
+    subject { controller.instance_exec(&Doorkeeper.configuration.authenticate_resource_owner) }
+
+    let(:controller) { double }
+
+    before do
+      allow(controller).to receive(:current_user).and_return(current_user)
+      allow(controller).to receive(:session).and_return({})
+      allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path'))
+      allow(controller).to receive(:redirect_to)
+      allow(controller).to receive(:new_user_session_url).and_return('/login')
+    end
+
+    context 'with a user present' do
+      let(:current_user) { create(:user) }
+
+      it 'returns the user' do
+        expect(subject).to eq current_user
+      end
+
+      it 'does not redirect' do
+        expect(controller).not_to receive(:redirect_to)
+
+        subject
+      end
+
+      it 'does not store the return path' do
+        subject
+
+        expect(controller.session).not_to include :user_return_to
+      end
+    end
+
+    context 'without a user present' do
+      let(:current_user) { nil }
+
+      # NOTE: this is required for doorkeeper-openid_connect
+      it 'returns nil' do
+        expect(subject).to eq nil
+      end
+
+      it 'redirects to the login form' do
+        expect(controller).to receive(:redirect_to).with('/login')
+
+        subject
+      end
+
+      it 'stores the return path' do
+        subject
+
+        expect(controller.session[:user_return_to]).to eq '/return-path'
+      end
+    end
+  end
+end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index ad7f032d1e5fb90a8a1d545aad2fb56034c4d559..65c97da2efd911238e358d631191ced10d408fe9 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do
 
   let(:secrets) { ActiveSupport::OrderedOptions.new }
 
+  HEX_KEY = /\h{128}/
+  RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
+
   before do
     allow(File).to receive(:write)
     allow(File).to receive(:delete)
@@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do
     allow(self).to receive(:exit)
   end
 
-  context 'setting secret_key_base and otp_key_base' do
+  context 'setting secret keys' do
     context 'when none of the secrets exist' do
       before do
         stub_env('SECRET_KEY_BASE', nil)
@@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do
         allow(self).to receive(:warn_missing_secret)
       end
 
-      it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+      it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
         create_tokens
 
         keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
 
         expect(keys.uniq).to eq(keys)
-        expect(keys.map(&:length)).to all(eq(128))
+        expect(keys).to all(match(HEX_KEY))
+      end
+
+      it 'generates an RSA key for jws_private_key' do
+        create_tokens
+
+        keys = secrets.values_at(:jws_private_key)
+
+        expect(keys.uniq).to eq(keys)
+        expect(keys).to all(match(RSA_KEY))
       end
 
       it 'warns about the secrets to add to secrets.yml' do
         expect(self).to receive(:warn_missing_secret).with('secret_key_base')
         expect(self).to receive(:warn_missing_secret).with('otp_key_base')
         expect(self).to receive(:warn_missing_secret).with('db_key_base')
+        expect(self).to receive(:warn_missing_secret).with('jws_private_key')
 
         create_tokens
       end
@@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do
           expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
           expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
           expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+          expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
         end
 
         create_tokens
@@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do
     context 'when the other secrets all exist' do
       before do
         secrets.db_key_base = 'db_key_base'
+        secrets.jws_private_key = 'jws_private_key'
 
         allow(File).to receive(:exist?).with('.secret').and_return(true)
         allow(File).to receive(:read).with('.secret').and_return('file_key')
@@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do
           stub_env('SECRET_KEY_BASE', 'env_key')
           secrets.secret_key_base = 'secret_key_base'
           secrets.otp_key_base = 'otp_key_base'
+          secrets.jws_private_key = 'jws_private_key'
         end
 
         it 'does not issue a warning' do
@@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do
         before do
           secrets.secret_key_base = 'secret_key_base'
           secrets.otp_key_base = 'otp_key_base'
+          secrets.jws_private_key = 'jws_private_key'
         end
 
         it 'does not write any files' do
@@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do
           expect(secrets.secret_key_base).to eq('secret_key_base')
           expect(secrets.otp_key_base).to eq('otp_key_base')
           expect(secrets.db_key_base).to eq('db_key_base')
+          expect(secrets.jws_private_key).to eq('jws_private_key')
         end
 
         it 'deletes the .secret file' do
@@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do
             expect(new_secrets['secret_key_base']).to eq('file_key')
             expect(new_secrets['otp_key_base']).to eq('file_key')
             expect(new_secrets['db_key_base']).to eq('db_key_base')
+            expect(new_secrets['jws_private_key']).to eq('jws_private_key')
           end
 
           create_tokens
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 290e47763eb286e69530d11f7928aa5a86900de5..ff8b8daa34798d16a67d086905dbf92606675ed6 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -27,7 +27,7 @@ describe 'trusted_proxies', lib: true do
 
   context 'with private IP ranges added' do
     before do
-      set_trusted_proxies([ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ])
+      set_trusted_proxies(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"])
     end
 
     it 'filters out private and local IPs' do
@@ -39,7 +39,7 @@ describe 'trusted_proxies', lib: true do
 
   context 'with proxy IP added' do
     before do
-      set_trusted_proxies([ "60.98.25.47" ])
+      set_trusted_proxies(["60.98.25.47"])
     end
 
     it 'filters out proxy IP' do
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index fbd9bb9f0ff8752530ae3668e1b78783f5ba813c..3d922021978aac8315cbbd33dea2a7dc772a1062 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,7 +18,8 @@
     "sandbox": false,
     "setFixtures": false,
     "setStyleFixtures": false,
-    "spyOnEvent": false
+    "spyOnEvent": false,
+    "ClassSpecHelper": false
   },
   "plugins": ["jasmine"],
   "rules": {
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js
similarity index 100%
rename from spec/javascripts/abuse_reports_spec.js.es6
rename to spec/javascripts/abuse_reports_spec.js
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js
similarity index 100%
rename from spec/javascripts/activities_spec.js.es6
rename to spec/javascripts/activities_spec.js
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a68bccb16f463ba942f01ad74b89b5a47e111902
--- /dev/null
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -0,0 +1,58 @@
+require('~/extensions/array');
+require('jquery');
+require('jquery-ujs');
+require('~/ajax_loading_spinner');
+
+describe('Ajax Loading Spinner', () => {
+  const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
+  preloadFixtures(fixtureTemplate);
+
+  beforeEach(() => {
+    loadFixtures(fixtureTemplate);
+    gl.AjaxLoadingSpinner.init();
+  });
+
+  it('change current icon with spinner icon and disable link while waiting ajax response', (done) => {
+    spyOn(jQuery, 'ajax').and.callFake((req) => {
+      const xhr = new XMLHttpRequest();
+      const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+      const icon = ajaxLoadingSpinner.querySelector('i');
+
+      req.beforeSend(xhr, { dataType: 'text/html' });
+
+      expect(icon).not.toHaveClass('fa-trash-o');
+      expect(icon).toHaveClass('fa-spinner');
+      expect(icon).toHaveClass('fa-spin');
+      expect(icon.dataset.icon).toEqual('fa-trash-o');
+      expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
+
+      req.complete({});
+
+      done();
+      const deferred = $.Deferred();
+      return deferred.promise();
+    });
+    document.querySelector('.js-ajax-loading-spinner').click();
+  });
+
+  it('use original icon again and enabled the link after complete the ajax request', (done) => {
+    spyOn(jQuery, 'ajax').and.callFake((req) => {
+      const xhr = new XMLHttpRequest();
+      const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+
+      req.beforeSend(xhr, { dataType: 'text/html' });
+      req.complete({});
+
+      const icon = ajaxLoadingSpinner.querySelector('i');
+      expect(icon).toHaveClass('fa-trash-o');
+      expect(icon).not.toHaveClass('fa-spinner');
+      expect(icon).not.toHaveClass('fa-spin');
+      expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
+
+      done();
+      const deferred = $.Deferred();
+      return deferred.promise();
+    });
+    document.querySelector('.js-ajax-loading-spinner').click();
+  });
+});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index e5826f9c29f70a4cb8ff196115191807d59eac45..ea7753c7a1d45a019ff648d354ca14ae24fc6938 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,11 +1,10 @@
 /* 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, max-len */
-/* global AwardsHandler */
 
-require('~/awards_handler');
-require('./fixtures/emoji_menu');
+import Cookies from 'js-cookie';
+import AwardsHandler from '~/awards_handler';
 
 (function() {
-  var awardsHandler, lazyAssert, urlRoot;
+  var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
 
   awardsHandler = null;
 
@@ -13,14 +12,6 @@ require('./fixtures/emoji_menu');
 
   window.gon || (window.gon = {});
 
-  gl.emojiAliases = function() {
-    return {
-      '+1': 'thumbsup',
-      '-1': 'thumbsdown'
-    };
-  };
-
-  gon.award_menu_url = '/emojis';
   urlRoot = gon.relative_url_root;
 
   lazyAssert = function(done, assertFn) {
@@ -32,22 +23,37 @@ require('./fixtures/emoji_menu');
   };
 
   describe('AwardsHandler', function() {
-    preloadFixtures('issues/open-issue.html.raw');
+    preloadFixtures('issues/issue_with_comment.html.raw');
     beforeEach(function() {
-      loadFixtures('issues/open-issue.html.raw');
+      loadFixtures('issues/issue_with_comment.html.raw');
       awardsHandler = new AwardsHandler;
       spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
         return function(url, emoji, cb) {
           return cb();
         };
       })(this));
-      spyOn(jQuery, 'get').and.callFake(function(req, cb) {
-        return cb(window.emojiMenu);
-      });
+
+      let isEmojiMenuBuilt = false;
+      openAndWaitForEmojiMenu = function() {
+        return new Promise((resolve, reject) => {
+          if (isEmojiMenuBuilt) {
+            resolve();
+          } else {
+            $('.js-add-award').eq(0).click();
+            const $menu = $('.emoji-menu');
+            $menu.one('build-emoji-menu-finish', () => {
+              isEmojiMenuBuilt = true;
+              resolve();
+            });
+          }
+        });
+      };
     });
     afterEach(function() {
       // restore original url root value
       gon.relative_url_root = urlRoot;
+
+      awardsHandler.destroy();
     });
     describe('::showEmojiMenu', function() {
       it('should show emoji menu when Add emoji button clicked', function(done) {
@@ -62,10 +68,9 @@ require('./fixtures/emoji_menu');
         });
       });
       it('should also show emoji menu for the smiley icon in notes', function(done) {
-        $('.note-action-button').click();
+        $('.js-add-award.note-action-button').click();
         return lazyAssert(done, function() {
-          var $emojiMenu;
-          $emojiMenu = $('.emoji-menu');
+          var $emojiMenu = $('.emoji-menu');
           return expect($emojiMenu.length).toBe(1);
         });
       });
@@ -86,7 +91,7 @@ require('./fixtures/emoji_menu');
         var $emojiButton, $votesBlock;
         $votesBlock = $('.js-awards-block').eq(0);
         awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
-        $emojiButton = $votesBlock.find('[data-emoji=heart]');
+        $emojiButton = $votesBlock.find('[data-name=heart]');
         expect($emojiButton.length).toBe(1);
         expect($emojiButton.next('.js-counter').text()).toBe('1');
         return expect($votesBlock.hasClass('hidden')).toBe(false);
@@ -96,14 +101,14 @@ require('./fixtures/emoji_menu');
         $votesBlock = $('.js-awards-block').eq(0);
         awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
         awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
-        $emojiButton = $votesBlock.find('[data-emoji=heart]');
+        $emojiButton = $votesBlock.find('[data-name=heart]');
         return expect($emojiButton.length).toBe(0);
       });
       return it('should decrement the emoji counter', function() {
         var $emojiButton, $votesBlock;
         $votesBlock = $('.js-awards-block').eq(0);
         awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
-        $emojiButton = $votesBlock.find('[data-emoji=heart]');
+        $emojiButton = $votesBlock.find('[data-name=heart]');
         $emojiButton.next('.js-counter').text(5);
         awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
         expect($emojiButton.length).toBe(1);
@@ -120,8 +125,8 @@ require('./fixtures/emoji_menu');
         var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
-        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
-        $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent();
+        $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
+        $thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').parent();
         awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
         expect($thumbsUpEmoji.hasClass('active')).toBe(true);
         expect($thumbsDownEmoji.hasClass('active')).toBe(false);
@@ -138,9 +143,9 @@ require('./fixtures/emoji_menu');
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
         awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
-        expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
-        awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
-        return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
+        expect($votesBlock.find('[data-name=fire]').length).toBe(1);
+        awardsHandler.removeEmoji($votesBlock.find('[data-name=fire]').closest('button'));
+        return expect($votesBlock.find('[data-name=fire]').length).toBe(0);
       });
     });
     describe('::addYouToUserList', function() {
@@ -148,7 +153,7 @@ require('./fixtures/emoji_menu');
         var $thumbsUpEmoji, $votesBlock, awardUrl;
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
-        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
         $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
         awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
         $thumbsUpEmoji.tooltip();
@@ -158,7 +163,7 @@ require('./fixtures/emoji_menu');
         var $thumbsUpEmoji, $votesBlock, awardUrl;
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
-        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
         $thumbsUpEmoji.attr('data-title', 'sam');
         awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
         $thumbsUpEmoji.tooltip();
@@ -170,7 +175,7 @@ require('./fixtures/emoji_menu');
         var $thumbsUpEmoji, $votesBlock, awardUrl;
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
-        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
         $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
         $thumbsUpEmoji.addClass('active');
         awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -181,7 +186,7 @@ require('./fixtures/emoji_menu');
         var $thumbsUpEmoji, $votesBlock, awardUrl;
         awardUrl = awardsHandler.getAwardUrl();
         $votesBlock = $('.js-awards-block').eq(0);
-        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
         $thumbsUpEmoji.attr('data-title', 'You and sam');
         $thumbsUpEmoji.addClass('active');
         awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -190,42 +195,111 @@ require('./fixtures/emoji_menu');
       });
     });
     describe('search', function() {
-      return it('should filter the emoji', function() {
-        $('.js-add-award').eq(0).click();
-        expect($('[data-emoji=angel]').is(':visible')).toBe(true);
-        expect($('[data-emoji=anger]').is(':visible')).toBe(true);
-        $('#emoji_search').val('ali').trigger('keyup');
-        expect($('[data-emoji=angel]').is(':visible')).toBe(false);
-        expect($('[data-emoji=anger]').is(':visible')).toBe(false);
-        return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
+      return it('should filter the emoji', function(done) {
+        return openAndWaitForEmojiMenu()
+          .then(() => {
+            expect($('[data-name=angel]').is(':visible')).toBe(true);
+            expect($('[data-name=anger]').is(':visible')).toBe(true);
+            $('#emoji_search').val('ali').trigger('input');
+            expect($('[data-name=angel]').is(':visible')).toBe(false);
+            expect($('[data-name=anger]').is(':visible')).toBe(false);
+            expect($('[data-name=alien]').is(':visible')).toBe(true);
+          })
+          .then(done)
+          .catch((err) => {
+            done.fail(`Failed to open and build emoji menu: ${err.message}`);
+          });
       });
     });
-    return describe('emoji menu', function() {
-      var openEmojiMenuAndAddEmoji, selector;
-      selector = '[data-emoji=sunglasses]';
-      openEmojiMenuAndAddEmoji = function() {
-        var $block, $emoji, $menu;
-        $('.js-add-award').eq(0).click();
-        $menu = $('.emoji-menu');
-        $block = $('.js-awards-block');
-        $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
-        expect($emoji.length).toBe(1);
-        expect($block.find(selector).length).toBe(0);
-        $emoji.click();
-        expect($menu.hasClass('.is-visible')).toBe(false);
-        return expect($block.find(selector).length).toBe(1);
+    describe('emoji menu', function() {
+      const emojiSelector = '[data-name="sunglasses"]';
+      const openEmojiMenuAndAddEmoji = function() {
+        return openAndWaitForEmojiMenu()
+          .then(() => {
+            const $menu = $('.emoji-menu');
+            const $block = $('.js-awards-block');
+            const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
+
+            expect($emoji.length).toBe(1);
+            expect($block.find(emojiSelector).length).toBe(0);
+            $emoji.click();
+            expect($menu.hasClass('.is-visible')).toBe(false);
+            expect($block.find(emojiSelector).length).toBe(1);
+          });
       };
-      it('should add selected emoji to awards block', function() {
-        return openEmojiMenuAndAddEmoji();
+      it('should add selected emoji to awards block', function(done) {
+        return openEmojiMenuAndAddEmoji()
+          .then(done)
+          .catch((err) => {
+            done.fail(`Failed to open and build emoji menu: ${err.message}`);
+          });
       });
-      return it('should remove already selected emoji', function() {
-        var $block, $emoji;
-        openEmojiMenuAndAddEmoji();
-        $('.js-add-award').eq(0).click();
-        $block = $('.js-awards-block');
-        $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
-        $emoji.click();
-        return expect($block.find(selector).length).toBe(0);
+      it('should remove already selected emoji', function(done) {
+        return openEmojiMenuAndAddEmoji()
+          .then(() => {
+            $('.js-add-award').eq(0).click();
+            const $block = $('.js-awards-block');
+            const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
+            $emoji.click();
+            expect($block.find(emojiSelector).length).toBe(0);
+          })
+          .then(done)
+          .catch((err) => {
+            done.fail(`Failed to open and build emoji menu: ${err.message}`);
+          });
+      });
+    });
+
+    describe('frequently used emojis', function() {
+      beforeEach(() => {
+        // Clear it out
+        Cookies.set('frequently_used_emojis', '');
+      });
+
+      it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', function(done) {
+        return openAndWaitForEmojiMenu()
+          .then(() => {
+            const emojiMenu = document.querySelector('.emoji-menu');
+            Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => {
+              expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used');
+            });
+          })
+          .then(done)
+          .catch((err) => {
+            done.fail(`Failed to open and build emoji menu: ${err.message}`);
+          });
+      });
+
+      it('should have any frequently used section when there are frequently used emojis', function(done) {
+        awardsHandler.addEmojiToFrequentlyUsedList('8ball');
+
+        return openAndWaitForEmojiMenu()
+          .then(() => {
+            const emojiMenu = document.querySelector('.emoji-menu');
+            const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title =>
+              title.textContent.trim().toLowerCase() === 'frequently used'
+            );
+
+            expect(hasFrequentlyUsedHeading).toBe(true);
+          })
+          .then(done)
+          .catch((err) => {
+            done.fail(`Failed to open and build emoji menu: ${err.message}`);
+          });
+      });
+
+      it('should disregard invalid frequently used emoji that are being attempted to be added', function() {
+        awardsHandler.addEmojiToFrequentlyUsedList('8ball');
+        awardsHandler.addEmojiToFrequentlyUsedList('invalid_emoji');
+        awardsHandler.addEmojiToFrequentlyUsedList('grinning');
+
+        expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
+      });
+
+      it('should disregard invalid frequently used emoji already set in cookie', function() {
+        Cookies.set('frequently_used_emojis', '8ball,invalid_emoji,grinning');
+
+        expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
       });
     });
   });
diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd9ab33289f067155b30e2f223ca1a13643969d8
--- /dev/null
+++ b/spec/javascripts/behaviors/bind_in_out_spec.js
@@ -0,0 +1,189 @@
+import BindInOut from '~/behaviors/bind_in_out';
+import ClassSpecHelper from '../helpers/class_spec_helper';
+
+describe('BindInOut', function () {
+  describe('.constructor', function () {
+    beforeEach(function () {
+      this.in = {};
+      this.out = {};
+
+      this.bindInOut = new BindInOut(this.in, this.out);
+    });
+
+    it('should set .in', function () {
+      expect(this.bindInOut.in).toBe(this.in);
+    });
+
+    it('should set .out', function () {
+      expect(this.bindInOut.out).toBe(this.out);
+    });
+
+    it('should set .eventWrapper', function () {
+      expect(this.bindInOut.eventWrapper).toEqual({});
+    });
+
+    describe('if .in is an input', function () {
+      beforeEach(function () {
+        this.bindInOut = new BindInOut({ tagName: 'INPUT' });
+      });
+
+      it('should set .eventType to keyup ', function () {
+        expect(this.bindInOut.eventType).toEqual('keyup');
+      });
+    });
+
+    describe('if .in is a textarea', function () {
+      beforeEach(function () {
+        this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' });
+      });
+
+      it('should set .eventType to keyup ', function () {
+        expect(this.bindInOut.eventType).toEqual('keyup');
+      });
+    });
+
+    describe('if .in is not an input or textarea', function () {
+      beforeEach(function () {
+        this.bindInOut = new BindInOut({ tagName: 'SELECT' });
+      });
+
+      it('should set .eventType to change ', function () {
+        expect(this.bindInOut.eventType).toEqual('change');
+      });
+    });
+  });
+
+  describe('.addEvents', function () {
+    beforeEach(function () {
+      this.in = jasmine.createSpyObj('in', ['addEventListener']);
+
+      this.bindInOut = new BindInOut(this.in);
+
+      this.addEvents = this.bindInOut.addEvents();
+    });
+
+    it('should set .eventWrapper.updateOut', function () {
+      expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function));
+    });
+
+    it('should call .addEventListener', function () {
+      expect(this.in.addEventListener)
+        .toHaveBeenCalledWith(
+          this.bindInOut.eventType,
+          this.bindInOut.eventWrapper.updateOut,
+        );
+    });
+
+    it('should return the instance', function () {
+      expect(this.addEvents).toBe(this.bindInOut);
+    });
+  });
+
+  describe('.updateOut', function () {
+    beforeEach(function () {
+      this.in = { value: 'the-value' };
+      this.out = { textContent: 'not-the-value' };
+
+      this.bindInOut = new BindInOut(this.in, this.out);
+
+      this.updateOut = this.bindInOut.updateOut();
+    });
+
+    it('should set .out.textContent to .in.value', function () {
+      expect(this.out.textContent).toBe(this.in.value);
+    });
+
+    it('should return the instance', function () {
+      expect(this.updateOut).toBe(this.bindInOut);
+    });
+  });
+
+  describe('.removeEvents', function () {
+    beforeEach(function () {
+      this.in = jasmine.createSpyObj('in', ['removeEventListener']);
+      this.updateOut = () => {};
+
+      this.bindInOut = new BindInOut(this.in);
+      this.bindInOut.eventWrapper.updateOut = this.updateOut;
+
+      this.removeEvents = this.bindInOut.removeEvents();
+    });
+
+    it('should call .removeEventListener', function () {
+      expect(this.in.removeEventListener)
+        .toHaveBeenCalledWith(
+          this.bindInOut.eventType,
+          this.updateOut,
+        );
+    });
+
+    it('should return the instance', function () {
+      expect(this.removeEvents).toBe(this.bindInOut);
+    });
+  });
+
+  describe('.initAll', function () {
+    beforeEach(function () {
+      this.ins = [0, 1, 2];
+      this.instances = [];
+
+      spyOn(document, 'querySelectorAll').and.returnValue(this.ins);
+      spyOn(Array.prototype, 'map').and.callThrough();
+      spyOn(BindInOut, 'init');
+
+      this.initAll = BindInOut.initAll();
+    });
+
+    ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll');
+
+    it('should call .querySelectorAll', function () {
+      expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]');
+    });
+
+    it('should call .map', function () {
+      expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function));
+    });
+
+    it('should call .init for each element', function () {
+      expect(BindInOut.init.calls.count()).toEqual(3);
+    });
+
+    it('should return an array of instances', function () {
+      expect(this.initAll).toEqual(jasmine.any(Array));
+    });
+  });
+
+  describe('.init', function () {
+    beforeEach(function () {
+      spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; });
+      spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; });
+
+      this.init = BindInOut.init({}, {});
+    });
+
+    ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init');
+
+    it('should call .addEvents', function () {
+      expect(BindInOut.prototype.addEvents).toHaveBeenCalled();
+    });
+
+    it('should call .updateOut', function () {
+      expect(BindInOut.prototype.updateOut).toHaveBeenCalled();
+    });
+
+    describe('if no anOut is provided', function () {
+      beforeEach(function () {
+        this.anIn = { dataset: { bindIn: 'the-data-bind-in' } };
+
+        spyOn(document, 'querySelector');
+
+        BindInOut.init(this.anIn);
+      });
+
+      it('should call .querySelector', function () {
+        expect(document.querySelector)
+          .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`);
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c1179e572ae44f6d738472b1abe968bfe83a15cd
--- /dev/null
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -0,0 +1,107 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('CreateBranchDropdown', () => {
+  const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+  // selectors
+  const createBranchSel = '.js-new-branch-btn';
+  const backBtnSel = '.dropdown-menu-back';
+  const cancelBtnSel = '.js-cancel-branch-btn';
+  const branchNameSel = '#new_branch_name';
+  const branchName = 'new_name';
+  let dropdown;
+
+  function createDropdown() {
+    const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+    const projectBranches = getJSONFixture('project_branches.json');
+    dropdown = new gl.TargetBranchDropDown(dropdownEl);
+    dropdown.cachedRefs = projectBranches;
+    return dropdown;
+  }
+
+  function createBranchBtn() {
+    return document.querySelector(createBranchSel);
+  }
+
+  function backBtn() {
+    return document.querySelector(backBtnSel);
+  }
+
+  function cancelBtn() {
+    return document.querySelector(cancelBtnSel);
+  }
+
+  function branchNameEl() {
+    return document.querySelector(branchNameSel);
+  }
+
+  function changeBranchName(text) {
+    branchNameEl().value = text;
+    branchNameEl().dispatchEvent(new Event('change'));
+  }
+
+  preloadFixtures(fixtureTemplate);
+
+  beforeEach(() => {
+    loadFixtures(fixtureTemplate);
+    createDropdown();
+  });
+
+  it('disable submit when branch name is empty', () => {
+    expect(createBranchBtn()).toBeDisabled();
+  });
+
+  it('enable submit when branch name is present', () => {
+    changeBranchName(branchName);
+
+    expect(createBranchBtn()).not.toBeDisabled();
+  });
+
+  it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
+    const spyBackEvent = spyOnEvent(backBtnSel, 'click');
+    changeBranchName(branchName);
+
+    cancelBtn().click();
+
+    expect(branchNameEl()).toHaveValue('');
+    expect(spyBackEvent).toHaveBeenTriggered();
+  });
+
+  it('resets the form when back btn is clicked', () => {
+    changeBranchName(branchName);
+
+    backBtn().click();
+
+    expect(branchNameEl()).toHaveValue('');
+  });
+
+  describe('new branch creation', () => {
+    beforeEach(() => {
+      changeBranchName(branchName);
+    });
+    it('sets the new branch name and updates the dropdown', () => {
+      spyOn(dropdown, 'setNewBranch');
+
+      createBranchBtn().click();
+
+      expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+    });
+
+    it('resets the form', () => {
+      createBranchBtn().click();
+
+      expect(branchNameEl()).toHaveValue('');
+    });
+
+    it('is triggered with enter keypress', () => {
+      spyOn(dropdown, 'setNewBranch');
+      const enterEvent = new Event('keydown');
+      enterEvent.which = 13;
+      branchNameEl().dispatchEvent(enterEvent);
+
+      expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+    });
+  });
+});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4fb79663c516516086f2aaf50b35925fe30f3114
--- /dev/null
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -0,0 +1,119 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('TargetBranchDropdown', () => {
+  const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+  let dropdown;
+
+  function createDropdown() {
+    const projectBranches = getJSONFixture('project_branches.json');
+    const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+    dropdown = new gl.TargetBranchDropDown(dropdownEl);
+    dropdown.cachedRefs = projectBranches;
+    dropdown.refreshData();
+    return dropdown;
+  }
+
+  function submitBtn() {
+    return document.querySelector('button[type="submit"]');
+  }
+
+  function searchField() {
+    return document.querySelector('.dropdown-page-one .dropdown-input-field');
+  }
+
+  function element() {
+    return document.querySelectorAll('div.dropdown-content li a');
+  }
+
+  function elementAtIndex(index) {
+    return element()[index];
+  }
+
+  function clickElementAtIndex(index) {
+    elementAtIndex(index).click();
+  }
+
+  preloadFixtures(fixtureTemplate);
+
+  beforeEach(() => {
+    loadFixtures(fixtureTemplate);
+    createDropdown();
+  });
+
+  it('disable submit when branch is not selected', () => {
+    document.querySelector('input[name="target_branch"]').value = null;
+    clickElementAtIndex(1);
+
+    expect(submitBtn().getAttribute('disabled')).toEqual('');
+  });
+
+  it('enable submit when a branch is selected', () => {
+    clickElementAtIndex(1);
+
+    expect(submitBtn().getAttribute('disabled')).toBe(null);
+  });
+
+  it('triggers change.branch event on a branch click', () => {
+    spyOnEvent(dropdown.$dropdown, 'change.branch');
+    clickElementAtIndex(0);
+
+    expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
+  });
+
+  describe('#dropdownData', () => {
+    it('cache the refs', () => {
+      const refs = dropdown.cachedRefs;
+      dropdown.cachedRefs = null;
+
+      dropdown.dropdownData(refs);
+
+      expect(dropdown.cachedRefs).toEqual(refs);
+    });
+
+    it('returns the Branches with the newBranch and defaultBranch', () => {
+      const refs = dropdown.cachedRefs;
+      dropdown.branchInput.value = 'master';
+      dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
+
+      const branches = dropdown.dropdownData(refs).Branches;
+
+      expect(branches.length).toEqual(4);
+      expect(branches[0]).toEqual(dropdown.newBranch);
+      expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
+      expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
+      expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
+    });
+  });
+
+  describe('#setNewBranch', () => {
+    it('adds the new branch and select it', () => {
+      const branchName = 'new_branch';
+
+      dropdown.setNewBranch(branchName);
+
+      expect(elementAtIndex(0)).toHaveClass('is-active');
+      expect(elementAtIndex(0)).toContainHtml(branchName);
+    });
+
+    it("doesn't add a new branch if already exists in the list", () => {
+      const branchName = elementAtIndex(0).text;
+      const initialLength = element().length;
+
+      dropdown.setNewBranch(branchName);
+
+      expect(element().length).toEqual(initialLength);
+    });
+
+    it('clears the search filter', () => {
+      const branchName = elementAtIndex(0).text;
+      searchField().value = 'searching';
+
+      dropdown.setNewBranch(branchName);
+
+      expect(searchField().value).toEqual('');
+    });
+  });
+});
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..47baf83512fa801aa55aad6b8ea7fda4210dd4de
--- /dev/null
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -0,0 +1,93 @@
+/* global BoardService */
+import Vue from 'vue';
+import '~/boards/stores/boards_store';
+import boardBlankState from '~/boards/components/board_blank_state';
+import './mock_data';
+
+describe('Boards blank state', () => {
+  let vm;
+  let fail = false;
+
+  beforeEach((done) => {
+    const Comp = Vue.extend(boardBlankState);
+
+    gl.issueBoards.BoardsStore.create();
+    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+
+    spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
+      if (fail) {
+        reject();
+      } else {
+        resolve({
+          json() {
+            return [{
+              id: 1,
+              title: 'To Do',
+              label: { id: 1 },
+            }, {
+              id: 2,
+              title: 'Doing',
+              label: { id: 2 },
+            }];
+          },
+        });
+      }
+    }));
+
+    vm = new Comp();
+
+    setTimeout(() => {
+      vm.$mount();
+      done();
+    });
+  });
+
+  it('renders pre-defined labels', () => {
+    expect(
+      vm.$el.querySelectorAll('.board-blank-state-list li').length,
+    ).toBe(2);
+
+    expect(
+      vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim(),
+    ).toEqual('To Do');
+
+    expect(
+      vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim(),
+    ).toEqual('Doing');
+  });
+
+  it('clears blank state', (done) => {
+    vm.$el.querySelector('.btn-default').click();
+
+    setTimeout(() => {
+      expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy();
+
+      done();
+    });
+  });
+
+  it('creates pre-defined labels', (done) => {
+    vm.$el.querySelector('.btn-create').click();
+
+    setTimeout(() => {
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+      expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do');
+      expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing');
+
+      done();
+    });
+  });
+
+  it('resets the store if request fails', (done) => {
+    fail = true;
+
+    vm.$el.querySelector('.btn-create').click();
+
+    setTimeout(() => {
+      expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy();
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+      done();
+    });
+  });
+});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..be31f644e20f1555ef55d896918d97b937a603ee
--- /dev/null
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -0,0 +1,168 @@
+/* global Vue */
+/* global List */
+/* global ListLabel */
+/* global listObj */
+/* global boardsMockInterceptor */
+/* global BoardService */
+
+require('~/boards/models/list');
+require('~/boards/models/label');
+require('~/boards/stores/boards_store');
+const boardCard = require('~/boards/components/board_card').default;
+require('./mock_data');
+
+describe('Issue card', () => {
+  let vm;
+
+  beforeEach((done) => {
+    Vue.http.interceptors.push(boardsMockInterceptor);
+
+    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+    gl.issueBoards.BoardsStore.create();
+    gl.issueBoards.BoardsStore.detail.issue = {};
+
+    const BoardCardComp = Vue.extend(boardCard);
+    const list = new List(listObj);
+    const label1 = new ListLabel({
+      id: 3,
+      title: 'testing 123',
+      color: 'blue',
+      text_color: 'white',
+      description: 'test',
+    });
+
+    setTimeout(() => {
+      list.issues[0].labels.push(label1);
+
+      vm = new BoardCardComp({
+        propsData: {
+          list,
+          issue: list.issues[0],
+          issueLinkBase: '/',
+          disabled: false,
+          index: 0,
+          rootPath: '/',
+        },
+      }).$mount();
+      done();
+    }, 0);
+  });
+
+  afterEach(() => {
+    Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+  });
+
+  it('returns false when detailIssue is empty', () => {
+    expect(vm.issueDetailVisible).toBe(false);
+  });
+
+  it('returns true when detailIssue is equal to card issue', () => {
+    gl.issueBoards.BoardsStore.detail.issue = vm.issue;
+
+    expect(vm.issueDetailVisible).toBe(true);
+  });
+
+  it('adds user-can-drag class if not disabled', () => {
+    expect(vm.$el.classList.contains('user-can-drag')).toBe(true);
+  });
+
+  it('does not add user-can-drag class disabled', (done) => {
+    vm.disabled = true;
+
+    setTimeout(() => {
+      expect(vm.$el.classList.contains('user-can-drag')).toBe(false);
+      done();
+    }, 0);
+  });
+
+  it('does not add disabled class', () => {
+    expect(vm.$el.classList.contains('is-disabled')).toBe(false);
+  });
+
+  it('adds disabled class is disabled is true', (done) => {
+    vm.disabled = true;
+
+    setTimeout(() => {
+      expect(vm.$el.classList.contains('is-disabled')).toBe(true);
+      done();
+    }, 0);
+  });
+
+  describe('mouse events', () => {
+    const triggerEvent = (eventName, el = vm.$el) => {
+      const event = document.createEvent('MouseEvents');
+      event.initMouseEvent(eventName, true, true, window, 1, 0, 0, 0, 0, false, false,
+                           false, false, 0, null);
+
+      el.dispatchEvent(event);
+    };
+
+    it('sets showDetail to true on mousedown', () => {
+      triggerEvent('mousedown');
+
+      expect(vm.showDetail).toBe(true);
+    });
+
+    it('sets showDetail to false on mousemove', () => {
+      triggerEvent('mousedown');
+
+      expect(vm.showDetail).toBe(true);
+
+      triggerEvent('mousemove');
+
+      expect(vm.showDetail).toBe(false);
+    });
+
+    it('does not set detail issue if showDetail is false', () => {
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+    });
+
+    it('does not set detail issue if link is clicked', () => {
+      triggerEvent('mouseup', vm.$el.querySelector('a'));
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+    });
+
+    it('does not set detail issue if button is clicked', () => {
+      triggerEvent('mouseup', vm.$el.querySelector('button'));
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+    });
+
+    it('does not set detail issue if showDetail is false after mouseup', () => {
+      triggerEvent('mouseup');
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+    });
+
+    it('sets detail issue to card issue on mouse up', () => {
+      triggerEvent('mousedown');
+      triggerEvent('mouseup');
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+      expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
+    });
+
+    it('adds active class if detail issue is set', (done) => {
+      triggerEvent('mousedown');
+      triggerEvent('mouseup');
+
+      setTimeout(() => {
+        expect(vm.$el.classList.contains('is-active')).toBe(true);
+        done();
+      }, 0);
+    });
+
+    it('resets detail issue to empty if already set', () => {
+      triggerEvent('mousedown');
+      triggerEvent('mouseup');
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+
+      triggerEvent('mousedown');
+      triggerEvent('mouseup');
+
+      expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+    });
+  });
+});
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4999933c0c175f0ce71cfd5b2674b62f6453e7ce
--- /dev/null
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -0,0 +1,190 @@
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global listObj */
+
+import Vue from 'vue';
+import boardNewIssue from '~/boards/components/board_new_issue';
+
+require('~/boards/models/list');
+require('./mock_data');
+
+describe('Issue boards new issue form', () => {
+  let vm;
+  let list;
+  const promiseReturn = {
+    json() {
+      return {
+        iid: 100,
+      };
+    },
+  };
+  const submitIssue = () => {
+    vm.$el.querySelector('.btn-success').click();
+  };
+
+  beforeEach((done) => {
+    const BoardNewIssueComp = Vue.extend(boardNewIssue);
+
+    Vue.http.interceptors.push(boardsMockInterceptor);
+    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+    gl.issueBoards.BoardsStore.create();
+    gl.IssueBoardsApp = new Vue();
+
+    setTimeout(() => {
+      list = new List(listObj);
+
+      spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => {
+        if (vm.title === 'error') {
+          reject();
+        } else {
+          resolve(promiseReturn);
+        }
+      }));
+
+      vm = new BoardNewIssueComp({
+        propsData: {
+          list,
+        },
+      }).$mount();
+
+      done();
+    }, 0);
+  });
+
+  afterEach(() => {
+    Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+  });
+
+  it('disables submit button if title is empty', () => {
+    expect(vm.$el.querySelector('.btn-success').disabled).toBe(true);
+  });
+
+  it('enables submit button if title is not empty', (done) => {
+    vm.title = 'Testing Title';
+
+    setTimeout(() => {
+      expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
+      expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
+
+      done();
+    }, 0);
+  });
+
+  it('clears title after clicking cancel', (done) => {
+    vm.$el.querySelector('.btn-default').click();
+
+    setTimeout(() => {
+      expect(vm.title).toBe('');
+      done();
+    }, 0);
+  });
+
+  it('does not create new issue if title is empty', (done) => {
+    submitIssue();
+
+    setTimeout(() => {
+      expect(gl.boardService.newIssue).not.toHaveBeenCalled();
+      done();
+    }, 0);
+  });
+
+  describe('submit success', () => {
+    it('creates new issue', (done) => {
+      vm.title = 'submit title';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(gl.boardService.newIssue).toHaveBeenCalled();
+        done();
+      }, 0);
+    });
+
+    it('enables button after submit', (done) => {
+      vm.title = 'submit issue';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true);
+        done();
+      }, 0);
+    });
+
+    it('clears title after submit', (done) => {
+      vm.title = 'submit issue';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(vm.title).toBe('');
+        done();
+      }, 0);
+    });
+
+    it('adds new issue to list after submit', (done) => {
+      vm.title = 'submit issue';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(list.issues.length).toBe(2);
+        expect(list.issues[1].title).toBe('submit issue');
+        expect(list.issues[1].subscribed).toBe(true);
+        done();
+      }, 0);
+    });
+
+    it('sets detail issue after submit', (done) => {
+      vm.title = 'submit issue';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue');
+        done();
+      });
+    });
+
+    it('sets detail list after submit', (done) => {
+      vm.title = 'submit issue';
+
+      setTimeout(() => {
+        submitIssue();
+
+        expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id);
+        done();
+      }, 0);
+    });
+  });
+
+  describe('submit error', () => {
+    it('removes issue', (done) => {
+      vm.title = 'error';
+
+      setTimeout(() => {
+        submitIssue();
+
+        setTimeout(() => {
+          expect(list.issues.length).toBe(1);
+          done();
+        }, 500);
+      }, 0);
+    });
+
+    it('shows error', (done) => {
+      vm.title = 'error';
+      submitIssue();
+
+      setTimeout(() => {
+        submitIssue();
+
+        setTimeout(() => {
+          expect(vm.error).toBe(true);
+          done();
+        }, 500);
+      }, 0);
+    });
+  });
+});
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js
similarity index 66%
rename from spec/javascripts/boards/boards_store_spec.js.es6
rename to spec/javascripts/boards/boards_store_spec.js
index 9dd741a680bc2b0c6d6cdc02918ad33c95d12025..1d1069600fc636ec94896515fd10bd97b810a518 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -5,6 +5,7 @@
 /* global Cookies */
 /* global listObj */
 /* global listObjDuplicate */
+/* global ListIssue */
 
 require('~/lib/utils/url_utility');
 require('~/boards/models/issue');
@@ -21,6 +22,10 @@ describe('Store', () => {
     gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
     gl.issueBoards.BoardsStore.create();
 
+    spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
+      resolve();
+    }));
+
     Cookies.set('issue_board_welcome_hidden', 'false', {
       expires: 365 * 10,
       path: ''
@@ -154,5 +159,74 @@ describe('Store', () => {
         done();
       }, 0);
     });
+
+    it('moves issue to top of another list', (done) => {
+      const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+      const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+      setTimeout(() => {
+        listOne.issues[0].id = 2;
+
+        expect(listOne.issues.length).toBe(1);
+        expect(listTwo.issues.length).toBe(1);
+
+        gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
+
+        expect(listOne.issues.length).toBe(0);
+        expect(listTwo.issues.length).toBe(2);
+        expect(listTwo.issues[0].id).toBe(2);
+        expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
+
+        done();
+      }, 0);
+    });
+
+    it('moves issue to bottom of another list', (done) => {
+      const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+      const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+      setTimeout(() => {
+        listOne.issues[0].id = 2;
+
+        expect(listOne.issues.length).toBe(1);
+        expect(listTwo.issues.length).toBe(1);
+
+        gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
+
+        expect(listOne.issues.length).toBe(0);
+        expect(listTwo.issues.length).toBe(2);
+        expect(listTwo.issues[1].id).toBe(2);
+        expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
+
+        done();
+      }, 0);
+    });
+
+    it('moves issue in list', (done) => {
+      const issue = new ListIssue({
+        title: 'Testing',
+        iid: 2,
+        confidential: false,
+        labels: []
+      });
+      const list = gl.issueBoards.BoardsStore.addList(listObj);
+
+      setTimeout(() => {
+        list.addIssue(issue);
+
+        expect(list.issues.length).toBe(2);
+
+        gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
+
+        expect(list.issues[0].id).toBe(2);
+        expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
+
+        done();
+      });
+    });
   });
 });
diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js
similarity index 100%
rename from spec/javascripts/boards/issue_card_spec.js.es6
rename to spec/javascripts/boards/issue_card_spec.js
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js
similarity index 82%
rename from spec/javascripts/boards/issue_spec.js.es6
rename to spec/javascripts/boards/issue_spec.js
index aab4d9c501e28fe4767b57a5e2fd96e0f3c86c8e..c96dfe94a4a5d35b1338c35809fcab87236d92f7 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js
@@ -79,4 +79,20 @@ describe('Issue model', () => {
     issue.removeLabels([issue.labels[0], issue.labels[1]]);
     expect(issue.labels.length).toBe(0);
   });
+
+  it('sets position to infinity if no position is stored', () => {
+    expect(issue.position).toBe(Infinity);
+  });
+
+  it('sets position', () => {
+    const relativePositionIssue = new ListIssue({
+      title: 'Testing',
+      iid: 1,
+      confidential: false,
+      relative_position: 1,
+      labels: []
+    });
+
+    expect(relativePositionIssue.position).toBe(1);
+  });
 });
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js
similarity index 77%
rename from spec/javascripts/boards/list_spec.js.es6
rename to spec/javascripts/boards/list_spec.js
index 4397a32fedcd3856bd2133e9d978e6c3415e7ee5..d49d3af33d9f5dd436e81daea8e01139023e02be 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js
@@ -3,7 +3,9 @@
 /* global boardsMockInterceptor */
 /* global BoardService */
 /* global List */
+/* global ListIssue */
 /* global listObj */
+/* global listObjDuplicate */
 
 require('~/lib/utils/url_utility');
 require('~/boards/models/issue');
@@ -84,4 +86,24 @@ describe('List model', () => {
       done();
     }, 0);
   });
+
+  it('sends service request to update issue label', () => {
+    const listDup = new List(listObjDuplicate);
+    const issue = new ListIssue({
+      title: 'Testing',
+      iid: 1,
+      confidential: false,
+      labels: [list.label, listDup.label]
+    });
+
+    list.issues.push(issue);
+    listDup.issues.push(issue);
+
+    spyOn(gl.boardService, 'moveIssue').and.callThrough();
+
+    listDup.updateIssueLabel(list, issue);
+
+    expect(gl.boardService.moveIssue)
+      .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
+  });
 });
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js
similarity index 100%
rename from spec/javascripts/boards/mock_data.js.es6
rename to spec/javascripts/boards/mock_data.js
diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js
similarity index 100%
rename from spec/javascripts/boards/modal_store_spec.js.es6
rename to spec/javascripts/boards/modal_store_spec.js
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js
similarity index 93%
rename from spec/javascripts/extensions/jquery_spec.js
rename to spec/javascripts/bootstrap_jquery_spec.js
index 096d3272eac164a1ac87f6906b4bce438832a060..48994b7c523308c2b379fe581c7926d952a47ce5 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/bootstrap_jquery_spec.js
@@ -1,9 +1,9 @@
 /* eslint-disable space-before-function-paren, no-var */
 
-require('~/extensions/jquery');
+import '~/commons/bootstrap';
 
 (function() {
-  describe('jQuery extensions', function() {
+  describe('Bootstrap jQuery extensions', function() {
     describe('disable', function() {
       beforeEach(function() {
         return setFixtures('<input type="text" />');
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js
similarity index 100%
rename from spec/javascripts/bootstrap_linked_tabs_spec.js.es6
rename to spec/javascripts/bootstrap_linked_tabs_spec.js
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js
similarity index 92%
rename from spec/javascripts/build_spec.js.es6
rename to spec/javascripts/build_spec.js
index 0bd50588f5a7d304ec16e942899a54447f84187f..549c7af8ea8dae77e979703d8ff3e62e414bbb4d 100644
--- a/spec/javascripts/build_spec.js.es6
+++ b/spec/javascripts/build_spec.js
@@ -9,12 +9,6 @@ require('vendor/jquery.nicescroll');
 
 describe('Build', () => {
   const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
-  // see spec/factories/ci/builds.rb
-  const BUILD_TRACE = 'BUILD TRACE';
-  // see lib/ci/ansi2html.rb
-  const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({
-    offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
-  }));
 
   preloadFixtures('builds/build-with-artifacts.html.raw');
 
@@ -23,7 +17,7 @@ describe('Build', () => {
     spyOn($, 'ajax');
   });
 
-  describe('constructor', () => {
+  describe('class constructor', () => {
     beforeEach(() => {
       jasmine.clock().install();
     });
@@ -42,7 +36,7 @@ describe('Build', () => {
         expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
         expect(this.build.buildStatus).toBe('success');
         expect(this.build.buildStage).toBe('test');
-        expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE);
+        expect(this.build.state).toBe('');
       });
 
       it('only shows the jobs matching the current stage', () => {
@@ -108,7 +102,7 @@ describe('Build', () => {
         expect($.ajax.calls.count()).toBe(2);
         let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
         expect(url).toBe(
-          `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`,
+          `${BUILD_URL}/trace.json?state=`,
         );
         expect(dataType).toBe('json');
         expect(success).toEqual(jasmine.any(Function));
diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js
similarity index 96%
rename from spec/javascripts/commit/pipelines/mock_data.js.es6
rename to spec/javascripts/commit/pipelines/mock_data.js
index 188908d66bdc5212a372275b79c4352e800f426a..82b00b4c1ec614c9a4fe8abeefb82abd3275bba5 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js.es6
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-unused-vars */
-const pipeline = {
+export default {
   id: 73,
   user: {
     name: 'Administrator',
@@ -88,5 +87,3 @@ const pipeline = {
   created_at: '2017-01-16T17:13:59.800Z',
   updated_at: '2017-01-25T00:00:17.132Z',
 };
-
-module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js
similarity index 83%
rename from spec/javascripts/commit/pipelines/pipelines_spec.js.es6
rename to spec/javascripts/commit/pipelines/pipelines_spec.js
index f09c57978a10d6329496d6709024fc6251147837..75efcc065853a5ae3b909cd3aafb89d535d11411 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,11 +1,6 @@
-/* global pipeline, Vue */
-
-require('~/flash');
-require('~/commit/pipelines/pipelines_store');
-require('~/commit/pipelines/pipelines_service');
-require('~/commit/pipelines/pipelines_table');
-require('~/vue_shared/vue_resource_interceptor');
-const pipeline = require('./mock_data');
+import Vue from 'vue';
+import PipelinesTable from '~/commit/pipelines/pipelines_table';
+import pipeline from './mock_data';
 
 describe('Pipelines table in Commits and Merge requests', () => {
   preloadFixtures('static/pipelines_table.html.raw');
@@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
       });
 
       it('should render the empty state', (done) => {
-        const component = new gl.commits.pipelines.PipelinesTableView({
+        const component = new PipelinesTable({
           el: document.querySelector('#commit-pipeline-table-view'),
         });
 
@@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
       });
 
       it('should render a table with the received pipelines', (done) => {
-        const component = new gl.commits.pipelines.PipelinesTableView({
+        const component = new PipelinesTable({
           el: document.querySelector('#commit-pipeline-table-view'),
         });
 
@@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
     });
 
     it('should render empty state', (done) => {
-      const component = new gl.commits.pipelines.PipelinesTableView({
+      const component = new PipelinesTable({
         el: document.querySelector('#commit-pipeline-table-view'),
       });
 
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
deleted file mode 100644
index 949734199790bd4c331cce8288de99f29d7640db..0000000000000000000000000000000000000000
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
+++ /dev/null
@@ -1,33 +0,0 @@
-const PipelinesStore = require('~/commit/pipelines/pipelines_store');
-
-describe('Store', () => {
-  let store;
-
-  beforeEach(() => {
-    store = new PipelinesStore();
-  });
-
-  // unregister intervals and event handlers
-  afterEach(() => gl.VueRealtimeListener.reset());
-
-  it('should start with a blank state', () => {
-    expect(store.state.pipelines.length).toBe(0);
-  });
-
-  it('should store an array of pipelines', () => {
-    const pipelines = [
-      {
-        id: '1',
-        name: 'pipeline',
-      },
-      {
-        id: '2',
-        name: 'pipeline_2',
-      },
-    ];
-
-    store.storePipelines(pipelines);
-
-    expect(store.state.pipelines.length).toBe(pipelines.length);
-  });
-});
diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js
similarity index 100%
rename from spec/javascripts/commits_spec.js.es6
rename to spec/javascripts/commits_spec.js
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js
similarity index 100%
rename from spec/javascripts/datetime_utility_spec.js.es6
rename to spec/javascripts/datetime_utility_spec.js
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js
similarity index 93%
rename from spec/javascripts/diff_comments_store_spec.js.es6
rename to spec/javascripts/diff_comments_store_spec.js
index f956394ef53607d5bed117780636db41489eaa46..84cf98c930aabd7d725d032e79ab0b24090ee1b6 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js
@@ -7,7 +7,16 @@ require('~/diff_notes/stores/comments');
 
 (() => {
   function createDiscussion(noteId = 1, resolved = true) {
-    CommentsStore.create('a', noteId, true, resolved, 'test');
+    CommentsStore.create({
+      discussionId: 'a',
+      noteId,
+      canResolve: true,
+      resolved,
+      resolvedBy: 'test',
+      authorName: 'test',
+      authorAvatar: 'test',
+      noteTruncated: 'test...',
+    });
   }
 
   beforeEach(() => {
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..85b73f1d4e2b8a86b8321f3670a20812a238cecd
--- /dev/null
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import actionsComp from '~/environments/components/environment_actions';
+
+describe('Actions Component', () => {
+  let ActionsComponent;
+  let actionsMock;
+  let spy;
+  let component;
+
+  beforeEach(() => {
+    ActionsComponent = Vue.extend(actionsComp);
+
+    actionsMock = [
+      {
+        name: 'bar',
+        play_path: 'https://gitlab.com/play',
+      },
+      {
+        name: 'foo',
+        play_path: '#',
+      },
+    ];
+
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+    component = new ActionsComponent({
+      propsData: {
+        actions: actionsMock,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a dropdown with the provided actions', () => {
+    expect(
+      component.$el.querySelectorAll('.dropdown-menu li').length,
+    ).toEqual(actionsMock.length);
+  });
+
+  it('should call the service when an action is clicked', () => {
+    component.$el.querySelector('.dropdown').click();
+    component.$el.querySelector('.js-manual-action-link').click();
+
+    expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path);
+  });
+});
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
deleted file mode 100644
index 850586f9f3acd964e45a86440ecf92fc5b9d969e..0000000000000000000000000000000000000000
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ /dev/null
@@ -1,66 +0,0 @@
-const ActionsComponent = require('~/environments/components/environment_actions');
-
-describe('Actions Component', () => {
-  preloadFixtures('static/environments/element.html.raw');
-
-  beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
-  });
-
-  it('should render a dropdown with the provided actions', () => {
-    const actionsMock = [
-      {
-        name: 'bar',
-        play_path: 'https://gitlab.com/play',
-      },
-      {
-        name: 'foo',
-        play_path: '#',
-      },
-    ];
-
-    const component = new ActionsComponent({
-      el: document.querySelector('.test-dom-element'),
-      propsData: {
-        actions: actionsMock,
-        playIconSvg: '<svg></svg>',
-      },
-    });
-
-    expect(
-      component.$el.querySelectorAll('.dropdown-menu li').length,
-    ).toEqual(actionsMock.length);
-    expect(
-      component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
-    ).toEqual(actionsMock[0].play_path);
-  });
-
-  it('should render a dropdown with the provided svg', () => {
-    const actionsMock = [
-      {
-        name: 'bar',
-        play_path: 'https://gitlab.com/play',
-      },
-      {
-        name: 'foo',
-        play_path: '#',
-      },
-    ];
-
-    const component = new ActionsComponent({
-      el: document.querySelector('.test-dom-element'),
-      propsData: {
-        actions: actionsMock,
-        playIconSvg: '<svg></svg>',
-      },
-    });
-
-    expect(
-      component.$el.querySelector('.js-dropdown-play-icon-container').children,
-    ).toContain('svg');
-
-    expect(
-      component.$el.querySelector('.js-action-play-icon-container').children,
-    ).toContain('svg');
-  });
-});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js
similarity index 62%
rename from spec/javascripts/environments/environment_external_url_spec.js.es6
rename to spec/javascripts/environments/environment_external_url_spec.js
index 393dbb5aae08be420c8ab879377e01f75c0bc2f7..9af218a27fff1691b1a38755622361b6168da848 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ b/spec/javascripts/environments/environment_external_url_spec.js
@@ -1,19 +1,20 @@
-const ExternalUrlComponent = require('~/environments/components/environment_external_url');
+import Vue from 'vue';
+import externalUrlComp from '~/environments/components/environment_external_url';
 
 describe('External URL Component', () => {
-  preloadFixtures('static/environments/element.html.raw');
+  let ExternalUrlComponent;
+
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    ExternalUrlComponent = Vue.extend(externalUrlComp);
   });
 
   it('should link to the provided externalUrl prop', () => {
     const externalURL = 'https://gitlab.com';
     const component = new ExternalUrlComponent({
-      el: document.querySelector('.test-dom-element'),
       propsData: {
         externalUrl: externalURL,
       },
-    });
+    }).$mount();
 
     expect(component.$el.getAttribute('href')).toEqual(externalURL);
     expect(component.$el.querySelector('fa-external-link')).toBeDefined();
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js
similarity index 94%
rename from spec/javascripts/environments/environment_item_spec.js.es6
rename to spec/javascripts/environments/environment_item_spec.js
index 7fea80ed799e579048f30b3439931314ba6e785f..4d42de4d54945d2124e8f64d8b76ff01f857b924 100644
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -1,10 +1,12 @@
-window.timeago = require('timeago.js');
-const EnvironmentItem = require('~/environments/components/environment_item');
+import 'timeago.js';
+import Vue from 'vue';
+import environmentItemComp from '~/environments/components/environment_item';
 
 describe('Environment item', () => {
-  preloadFixtures('static/environments/table.html.raw');
+  let EnvironmentItem;
+
   beforeEach(() => {
-    loadFixtures('static/environments/table.html.raw');
+    EnvironmentItem = Vue.extend(environmentItemComp);
   });
 
   describe('When item is folder', () => {
@@ -21,13 +23,13 @@ describe('Environment item', () => {
       };
 
       component = new EnvironmentItem({
-        el: document.querySelector('tr#environment-row'),
         propsData: {
           model: mockItem,
           canCreateDeployment: false,
           canReadEnvironment: true,
+          service: {},
         },
-      });
+      }).$mount();
     });
 
     it('Should render folder icon and name', () => {
@@ -109,13 +111,13 @@ describe('Environment item', () => {
       };
 
       component = new EnvironmentItem({
-        el: document.querySelector('tr#environment-row'),
         propsData: {
           model: environment,
           canCreateDeployment: true,
           canReadEnvironment: true,
+          service: {},
         },
-      });
+      }).$mount();
     });
 
     it('should render environment name', () => {
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js
similarity index 59%
rename from spec/javascripts/environments/environment_rollback_spec.js.es6
rename to spec/javascripts/environments/environment_rollback_spec.js
index 4a596baad09cfdf52157b11626cfc6945d5d46c9..7cb39d9df03ddec6d1e47b7d863b02eb5fa694ca 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -1,47 +1,59 @@
-const RollbackComponent = require('~/environments/components/environment_rollback');
+import Vue from 'vue';
+import rollbackComp from '~/environments/components/environment_rollback';
 
 describe('Rollback Component', () => {
-  preloadFixtures('static/environments/element.html.raw');
-
   const retryURL = 'https://gitlab.com/retry';
+  let RollbackComponent;
+  let spy;
 
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    RollbackComponent = Vue.extend(rollbackComp);
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
   });
 
-  it('Should link to the provided retryUrl', () => {
+  it('Should render Re-deploy label when isLastDeployment is true', () => {
     const component = new RollbackComponent({
       el: document.querySelector('.test-dom-element'),
       propsData: {
         retryUrl: retryURL,
         isLastDeployment: true,
+        service: {
+          postAction: spy,
+        },
       },
-    });
+    }).$mount();
 
-    expect(component.$el.getAttribute('href')).toEqual(retryURL);
+    expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
   });
 
-  it('Should render Re-deploy label when isLastDeployment is true', () => {
+  it('Should render Rollback label when isLastDeployment is false', () => {
     const component = new RollbackComponent({
       el: document.querySelector('.test-dom-element'),
       propsData: {
         retryUrl: retryURL,
-        isLastDeployment: true,
+        isLastDeployment: false,
+        service: {
+          postAction: spy,
+        },
       },
-    });
+    }).$mount();
 
-    expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+    expect(component.$el.querySelector('span').textContent).toContain('Rollback');
   });
 
-  it('Should render Rollback label when isLastDeployment is false', () => {
+  it('should call the service when the button is clicked', () => {
     const component = new RollbackComponent({
-      el: document.querySelector('.test-dom-element'),
       propsData: {
         retryUrl: retryURL,
         isLastDeployment: false,
+        service: {
+          postAction: spy,
+        },
       },
-    });
+    }).$mount();
 
-    expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+    component.$el.click();
+
+    expect(spy).toHaveBeenCalledWith(retryURL);
   });
 });
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js
similarity index 96%
rename from spec/javascripts/environments/environment_spec.js.es6
rename to spec/javascripts/environments/environment_spec.js
index edd0cad32d08afaad849e021aef3537b7b291e90..9601575577ee8cbd93d683b1000757bd50616535 100644
--- a/spec/javascripts/environments/environment_spec.js.es6
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,7 +1,7 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsComponent = require('~/environments/components/environment');
-const { environment } = require('./mock_data');
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsComponent from '~/environments/components/environment';
+import { environment } from './mock_data';
 
 describe('Environment', () => {
   preloadFixtures('static/environments/environments.html.raw');
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f79b88f3df1c073b6be3ac60fcb7c1f86bdfb02
--- /dev/null
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import stopComp from '~/environments/components/environment_stop';
+
+describe('Stop Component', () => {
+  let StopComponent;
+  let component;
+  let spy;
+  const stopURL = '/stop';
+
+  beforeEach(() => {
+    StopComponent = Vue.extend(stopComp);
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+    spyOn(window, 'confirm').and.returnValue(true);
+
+    component = new StopComponent({
+      propsData: {
+        stopUrl: stopURL,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a button to stop the environment', () => {
+    expect(component.$el.tagName).toEqual('BUTTON');
+    expect(component.$el.getAttribute('title')).toEqual('Stop Environment');
+  });
+
+  it('should call the service when an action is clicked', () => {
+    component.$el.click();
+    expect(spy).toHaveBeenCalled();
+  });
+});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
deleted file mode 100644
index 5ca65b1debc32e61f817929ca812b1b352dd9262..0000000000000000000000000000000000000000
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ /dev/null
@@ -1,28 +0,0 @@
-const StopComponent = require('~/environments/components/environment_stop');
-
-describe('Stop Component', () => {
-  preloadFixtures('static/environments/element.html.raw');
-
-  let stopURL;
-  let component;
-
-  beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
-
-    stopURL = '/stop';
-    component = new StopComponent({
-      el: document.querySelector('.test-dom-element'),
-      propsData: {
-        stopUrl: stopURL,
-      },
-    });
-  });
-
-  it('should link to the provided URL', () => {
-    expect(component.$el.getAttribute('href')).toEqual(stopURL);
-  });
-
-  it('should have a data-confirm attribute', () => {
-    expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
-  });
-});
diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js
similarity index 76%
rename from spec/javascripts/environments/environment_table_spec.js.es6
rename to spec/javascripts/environments/environment_table_spec.js
index be4330b50124a9e87ae16fa871ef4f8728d85b89..3df967848a76cb303551e9e2ed5cd6969f3a7d2b 100644
--- a/spec/javascripts/environments/environment_table_spec.js.es6
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -1,4 +1,5 @@
-const EnvironmentTable = require('~/environments/components/environments_table');
+import Vue from 'vue';
+import environmentTableComp from '~/environments/components/environments_table';
 
 describe('Environment item', () => {
   preloadFixtures('static/environments/element.html.raw');
@@ -16,14 +17,17 @@ describe('Environment item', () => {
       },
     };
 
+    const EnvironmentTable = Vue.extend(environmentTableComp);
+
     const component = new EnvironmentTable({
       el: document.querySelector('.test-dom-element'),
       propsData: {
         environments: [{ mockItem }],
         canCreateDeployment: false,
         canReadEnvironment: true,
+        service: {},
       },
-    });
+    }).$mount();
 
     expect(component.$el.tagName).toEqual('TABLE');
   });
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b07aa4e1745624ee82906bdeaba961b72da5f479
--- /dev/null
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import terminalComp from '~/environments/components/environment_terminal_button';
+
+describe('Stop Component', () => {
+  let TerminalComponent;
+  let component;
+  const terminalPath = '/path';
+
+  beforeEach(() => {
+    TerminalComponent = Vue.extend(terminalComp);
+
+    component = new TerminalComponent({
+      propsData: {
+        terminalPath,
+      },
+    }).$mount();
+  });
+
+  it('should render a link to open a web terminal with the provided path', () => {
+    expect(component.$el.tagName).toEqual('A');
+    expect(component.$el.getAttribute('title')).toEqual('Open web terminal');
+    expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+  });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js
similarity index 91%
rename from spec/javascripts/environments/environments_store_spec.js.es6
rename to spec/javascripts/environments/environments_store_spec.js
index 77e182b3830781efb874e687da73db79e3c94282..115d84b50f5dcebe51238e6384dc03f012cbde79 100644
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -1,5 +1,5 @@
-const Store = require('~/environments/stores/environments_store');
-const { environmentsList, serverData } = require('./mock_data');
+import Store from '~/environments/stores/environments_store';
+import { environmentsList, serverData } from './mock_data';
 
 (() => {
   describe('Store', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js
similarity index 96%
rename from spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
rename to spec/javascripts/environments/folder/environments_folder_view_spec.js
index d1335b5b3040da4c78500d601ca45dac0766accc..43a217a67f51f6ecb3dcdf9cd2309f4bb42f77dc 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,7 +1,7 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
-const { environmentsList } = require('../mock_data');
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import { environmentsList } from '../mock_data';
 
 describe('Environments Folder View', () => {
   preloadFixtures('static/environments/environments_folder_view.html.raw');
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js
similarity index 93%
rename from spec/javascripts/environments/mock_data.js.es6
rename to spec/javascripts/environments/mock_data.js
index 5c395c6b2d89b733ffc73a5f06f4c81764ec775a..30861481cc54e9acf7fa283148c321c55df09177 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js
@@ -1,4 +1,4 @@
-const environmentsList = [
+export const environmentsList = [
   {
     name: 'DEV',
     size: 1,
@@ -30,7 +30,7 @@ const environmentsList = [
   },
 ];
 
-const serverData = [
+export const serverData = [
   {
     name: 'DEV',
     size: 1,
@@ -67,7 +67,7 @@ const serverData = [
   },
 ];
 
-const environment = {
+export const environment = {
   name: 'DEV',
   size: 1,
   latest: {
@@ -84,9 +84,3 @@ const environment = {
     updated_at: '2017-01-31T10:53:46.894Z',
   },
 };
-
-module.exports = {
-  environmentsList,
-  environment,
-  serverData,
-};
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b871fe967d536d8a1ffe9ef20cac427b291b4fa
--- /dev/null
+++ b/spec/javascripts/extensions/array_spec.js
@@ -0,0 +1,22 @@
+/* eslint-disable space-before-function-paren, no-var */
+
+require('~/extensions/array');
+
+(function() {
+  describe('Array extensions', function() {
+    describe('first', function() {
+      return it('returns the first item', function() {
+        var arr;
+        arr = [0, 1, 2, 3, 4, 5];
+        return expect(arr.first()).toBe(0);
+      });
+    });
+    describe('last', function() {
+      return it('returns the last item', function() {
+        var arr;
+        arr = [0, 1, 2, 3, 4, 5];
+        return expect(arr.last()).toBe(5);
+      });
+    });
+  });
+}).call(window);
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
deleted file mode 100644
index 60f6b9b78e3b77f9fd09feefaf991b3cf13891a2..0000000000000000000000000000000000000000
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable space-before-function-paren, no-var */
-
-require('~/extensions/array');
-
-(function() {
-  describe('Array extensions', function() {
-    describe('first', function() {
-      return it('returns the first item', function() {
-        var arr;
-        arr = [0, 1, 2, 3, 4, 5];
-        return expect(arr.first()).toBe(0);
-      });
-    });
-    describe('last', function() {
-      return it('returns the last item', function() {
-        var arr;
-        arr = [0, 1, 2, 3, 4, 5];
-        return expect(arr.last()).toBe(5);
-      });
-    });
-
-    describe('find', function () {
-      beforeEach(() => {
-        this.arr = [0, 1, 2, 3, 4, 5];
-      });
-
-      it('returns the item that first passes the predicate function', () => {
-        expect(this.arr.find(item => item === 2)).toBe(2);
-      });
-
-      it('returns undefined if no items pass the predicate function', () => {
-        expect(this.arr.find(item => item === 6)).not.toBeDefined();
-      });
-
-      it('error when called on undefined or null', () => {
-        expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow();
-        expect(Array.prototype.find.bind(null, item => item === 1)).toThrow();
-      });
-
-      it('error when predicate is not a function', () => {
-        expect(Array.prototype.find.bind(this.arr, 1)).toThrow();
-      });
-    });
-  });
-}).call(window);
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
deleted file mode 100644
index 2d8a128ed330964ad91645d8b60721411d4b5424..0000000000000000000000000000000000000000
--- a/spec/javascripts/extensions/element_spec.js.es6
+++ /dev/null
@@ -1,38 +0,0 @@
-require('~/extensions/element');
-
-(() => {
-  describe('Element extensions', function () {
-    beforeEach(() => {
-      this.element = document.createElement('ul');
-    });
-
-    describe('matches', () => {
-      it('returns true if element matches the selector', () => {
-        expect(this.element.matches('ul')).toBeTruthy();
-      });
-
-      it("returns false if element doesn't match the selector", () => {
-        expect(this.element.matches('.not-an-element')).toBeFalsy();
-      });
-    });
-
-    describe('closest', () => {
-      beforeEach(() => {
-        this.childElement = document.createElement('li');
-        this.element.appendChild(this.childElement);
-      });
-
-      it('returns the closest parent that matches the selector', () => {
-        expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
-      });
-
-      it('returns itself if it matches the selector', () => {
-        expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
-      });
-
-      it('returns undefined if nothing matches the selector', () => {
-        expect(this.childElement.closest('.no-an-element')).toBeFalsy();
-      });
-    });
-  });
-})();
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
deleted file mode 100644
index 2467ed78459d090c3b502cc223c497576451f619..0000000000000000000000000000000000000000
--- a/spec/javascripts/extensions/object_spec.js.es6
+++ /dev/null
@@ -1,25 +0,0 @@
-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/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js
similarity index 93%
rename from spec/javascripts/filtered_search/dropdown_user_spec.js.es6
rename to spec/javascripts/filtered_search/dropdown_user_spec.js
index fa9d03c8a9af14c10b4dd8862c20d94c6b69aee0..c16f77c53a2603bfb165891f7aca60f08d4816c7 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
 
       it('should not return the double quote found in value', () => {
         spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
-          lastToken: {
-            value: '"johnny appleseed',
-          },
+          lastToken: '"johnny appleseed',
         });
 
         expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
 
       it('should not return the single quote found in value', () => {
         spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
-          lastToken: {
-            value: '\'larry boy',
-          },
+          lastToken: '\'larry boy',
         });
 
         expect(dropdownUser.getSearchInput()).toBe('larry boy');
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js
similarity index 87%
rename from spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
rename to spec/javascripts/filtered_search/dropdown_utils_spec.js
index 1e2d7582d5bbdf699478c52c4dea2bf929add1d6..e653802089646e16d92abe721dfff66a969d6352 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
       });
 
       it('should filter without symbol', () => {
-        input.value = ':roo';
+        input.value = 'roo';
 
         const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
         expect(updatedItem.droplab_hidden).toBe(false);
@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
         expect(updatedItem.droplab_hidden).toBe(false);
       });
 
-      it('should filter with colon', () => {
-        input.value = 'roo';
-
-        const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
-        expect(updatedItem.droplab_hidden).toBe(false);
-      });
-
       describe('filters multiple word title', () => {
         const multipleWordItem = {
           title: 'Community Contributions',
         };
 
         it('should filter with double quote', () => {
-          input.value = 'label:"';
+          input.value = '"';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with double quote and symbol', () => {
-          input.value = 'label:~"';
+          input.value = '~"';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with double quote and multiple words', () => {
-          input.value = 'label:"community con';
+          input.value = '"community con';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with double quote, symbol and multiple words', () => {
-          input.value = 'label:~"community con';
+          input.value = '~"community con';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with single quote', () => {
-          input.value = 'label:\'';
+          input.value = '\'';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with single quote and symbol', () => {
-          input.value = 'label:~\'';
+          input.value = '~\'';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with single quote and multiple words', () => {
-          input.value = 'label:\'community con';
+          input.value = '\'community con';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
         });
 
         it('should filter with single quote, symbol and multiple words', () => {
-          input.value = 'label:~\'community con';
+          input.value = '~\'community con';
 
           const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
           expect(updatedItem.droplab_hidden).toBe(false);
@@ -133,7 +126,11 @@ require('~/filtered_search/filtered_search_dropdown_manager');
 
       beforeEach(() => {
         setFixtures(`
-          <input type="text" id="test" />
+          <ul class="tokens-container">
+            <li class="input-token">
+              <input class="filtered-search" type="text" id="test" />
+            </li>
+          </ul>
         `);
 
         input = document.getElementById('test');
@@ -149,7 +146,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
         input.value = 'o';
         updatedItem = gl.DropdownUtils.filterHint(input, {
           hint: 'label',
-        }, 'o');
+        });
         expect(updatedItem.droplab_hidden).toBe(true);
       });
 
@@ -157,6 +154,29 @@ require('~/filtered_search/filtered_search_dropdown_manager');
         const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
         expect(updatedItem.droplab_hidden).toBe(false);
       });
+
+      it('should allow multiple if item.type is array', () => {
+        input.value = 'label:~first la';
+        const updatedItem = gl.DropdownUtils.filterHint(input, {
+          hint: 'label',
+          type: 'array',
+        });
+        expect(updatedItem.droplab_hidden).toBe(false);
+      });
+
+      it('should prevent multiple if item.type is not array', () => {
+        input.value = 'milestone:~first mile';
+        let updatedItem = gl.DropdownUtils.filterHint(input, {
+          hint: 'milestone',
+        });
+        expect(updatedItem.droplab_hidden).toBe(true);
+
+        updatedItem = gl.DropdownUtils.filterHint(input, {
+          hint: 'milestone',
+          type: 'string',
+        });
+        expect(updatedItem.droplab_hidden).toBe(true);
+      });
     });
 
     describe('setDataValueIfSelected', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1da3396d7b7c971483d8e9898c0d8c9cb97ff30
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -0,0 +1,101 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_visual_tokens');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+
+(() => {
+  describe('Filtered Search Dropdown Manager', () => {
+    describe('addWordToInput', () => {
+      function getInputValue() {
+        return document.querySelector('.filtered-search').value;
+      }
+
+      function setInputValue(value) {
+        document.querySelector('.filtered-search').value = value;
+      }
+
+      beforeEach(() => {
+        setFixtures(`
+          <ul class="tokens-container">
+            <li class="input-token">
+              <input class="filtered-search">
+            </li>
+          </ul>
+        `);
+      });
+
+      describe('input has no existing value', () => {
+        it('should add just tokenName', () => {
+          gl.FilteredSearchDropdownManager.addWordToInput('milestone');
+
+          const token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('milestone');
+          expect(getInputValue()).toBe('');
+        });
+
+        it('should add tokenName and tokenValue', () => {
+          gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+          let token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('label');
+          expect(getInputValue()).toBe('');
+
+          gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
+          // We have to get that reference again
+          // Because gl.FilteredSearchDropdownManager deletes the previous token
+          token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('label');
+          expect(token.querySelector('.value').innerText).toBe('none');
+          expect(getInputValue()).toBe('');
+        });
+      });
+
+      describe('input has existing value', () => {
+        it('should be able to just add tokenName', () => {
+          setInputValue('a');
+          gl.FilteredSearchDropdownManager.addWordToInput('author');
+
+          const token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('author');
+          expect(getInputValue()).toBe('');
+        });
+
+        it('should replace tokenValue', () => {
+          gl.FilteredSearchDropdownManager.addWordToInput('author');
+
+          setInputValue('roo');
+          gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
+
+          const token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('author');
+          expect(token.querySelector('.value').innerText).toBe('@root');
+          expect(getInputValue()).toBe('');
+        });
+
+        it('should add tokenValues containing spaces', () => {
+          gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+          setInputValue('"test ');
+          gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
+
+          const token = document.querySelector('.tokens-container .js-visual-token');
+
+          expect(token.classList.contains('filtered-search-token')).toEqual(true);
+          expect(token.querySelector('.name').innerText).toBe('label');
+          expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
+          expect(getInputValue()).toBe('');
+        });
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
deleted file mode 100644
index ed0b0196ec426c2b29625f7a0d854221c5c3f96f..0000000000000000000000000000000000000000
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
+++ /dev/null
@@ -1,59 +0,0 @@
-require('~/extensions/array');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown_manager');
-
-(() => {
-  describe('Filtered Search Dropdown Manager', () => {
-    describe('addWordToInput', () => {
-      function getInputValue() {
-        return document.querySelector('.filtered-search').value;
-      }
-
-      function setInputValue(value) {
-        document.querySelector('.filtered-search').value = value;
-      }
-
-      beforeEach(() => {
-        const input = document.createElement('input');
-        input.classList.add('filtered-search');
-        document.body.appendChild(input);
-      });
-
-      afterEach(() => {
-        document.querySelector('.filtered-search').outerHTML = '';
-      });
-
-      describe('input has no existing value', () => {
-        it('should add just tokenName', () => {
-          gl.FilteredSearchDropdownManager.addWordToInput('milestone');
-          expect(getInputValue()).toBe('milestone:');
-        });
-
-        it('should add tokenName and tokenValue', () => {
-          gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
-          expect(getInputValue()).toBe('label:none ');
-        });
-      });
-
-      describe('input has existing value', () => {
-        it('should be able to just add tokenName', () => {
-          setInputValue('a');
-          gl.FilteredSearchDropdownManager.addWordToInput('author');
-          expect(getInputValue()).toBe('author:');
-        });
-
-        it('should replace tokenValue', () => {
-          setInputValue('author:roo');
-          gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
-          expect(getInputValue()).toBe('author:@root ');
-        });
-
-        it('should add tokenValues containing spaces', () => {
-          setInputValue('label:~"test');
-          gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
-          expect(getInputValue()).toBe('label:~\'"test me"\' ');
-        });
-      });
-    });
-  });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..113161c21c60d5995888c8d7c63552af6c68ac26
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -0,0 +1,262 @@
+require('~/lib/utils/url_utility');
+require('~/lib/utils/common_utils');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+require('~/filtered_search/filtered_search_manager');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
+
+(() => {
+  describe('Filtered Search Manager', () => {
+    let input;
+    let manager;
+    let tokensContainer;
+    const placeholder = 'Search or filter results...';
+
+    function dispatchBackspaceEvent(element, eventType) {
+      const backspaceKey = 8;
+      const event = new Event(eventType);
+      event.keyCode = backspaceKey;
+      element.dispatchEvent(event);
+    }
+
+    function dispatchDeleteEvent(element, eventType) {
+      const deleteKey = 46;
+      const event = new Event(eventType);
+      event.keyCode = deleteKey;
+      element.dispatchEvent(event);
+    }
+
+    beforeEach(() => {
+      setFixtures(`
+        <div class="filtered-search-input-container">
+          <form>
+            <ul class="tokens-container list-unstyled">
+              ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+            </ul>
+            <button class="clear-search" type="button">
+              <i class="fa fa-times"></i>
+            </button>
+          </form>
+        </div>
+      `);
+
+      spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
+      spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
+      spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
+      spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
+      spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+      spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
+
+      input = document.querySelector('.filtered-search');
+      tokensContainer = document.querySelector('.tokens-container');
+      manager = new gl.FilteredSearchManager();
+    });
+
+    afterEach(() => {
+      manager.cleanup();
+    });
+
+    describe('search', () => {
+      const defaultParams = '?scope=all&utf8=✓&state=opened';
+
+      it('should search with a single word', (done) => {
+        input.value = 'searchTerm';
+
+        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+          expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+          done();
+        });
+
+        manager.search();
+      });
+
+      it('should search with multiple words', (done) => {
+        input.value = 'awesome search terms';
+
+        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+          expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+          done();
+        });
+
+        manager.search();
+      });
+
+      it('should search with special characters', (done) => {
+        input.value = '~!@#$%^&*()_+{}:<>,.?/';
+
+        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+          expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+          done();
+        });
+
+        manager.search();
+      });
+    });
+
+    describe('handleInputPlaceholder', () => {
+      it('should render placeholder when there is no input', () => {
+        expect(input.placeholder).toEqual(placeholder);
+      });
+
+      it('should not render placeholder when there is input', () => {
+        input.value = 'test words';
+
+        const event = new Event('input');
+        input.dispatchEvent(event);
+
+        expect(input.placeholder).toEqual('');
+      });
+
+      it('should not render placeholder when there are tokens and no input', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+          FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+        );
+
+        const event = new Event('input');
+        input.dispatchEvent(event);
+
+        expect(input.placeholder).toEqual('');
+      });
+    });
+
+    describe('checkForBackspace', () => {
+      describe('tokens and no input', () => {
+        beforeEach(() => {
+          tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+            FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+          );
+        });
+
+        it('removes last token', () => {
+          spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+          dispatchBackspaceEvent(input, 'keyup');
+
+          expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+        });
+
+        it('sets the input', () => {
+          spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+          dispatchDeleteEvent(input, 'keyup');
+
+          expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+          expect(input.value).toEqual('~bug');
+        });
+      });
+
+      it('does not remove token or change input when there is existing input', () => {
+        spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+        spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+
+        input.value = 'text';
+        dispatchDeleteEvent(input, 'keyup');
+
+        expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+        expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+        expect(input.value).toEqual('text');
+      });
+    });
+
+    describe('removeSelectedToken', () => {
+      function getVisualTokens() {
+        return tokensContainer.querySelectorAll('.js-visual-token');
+      }
+
+      beforeEach(() => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+          FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+        );
+      });
+
+      it('removes selected token when the backspace key is pressed', () => {
+        expect(getVisualTokens().length).toEqual(1);
+
+        dispatchBackspaceEvent(document, 'keydown');
+
+        expect(getVisualTokens().length).toEqual(0);
+      });
+
+      it('removes selected token when the delete key is pressed', () => {
+        expect(getVisualTokens().length).toEqual(1);
+
+        dispatchDeleteEvent(document, 'keydown');
+
+        expect(getVisualTokens().length).toEqual(0);
+      });
+
+      it('updates the input placeholder after removal', () => {
+        manager.handleInputPlaceholder();
+
+        expect(input.placeholder).toEqual('');
+        expect(getVisualTokens().length).toEqual(1);
+
+        dispatchBackspaceEvent(document, 'keydown');
+
+        expect(input.placeholder).not.toEqual('');
+        expect(getVisualTokens().length).toEqual(0);
+      });
+
+      it('updates the clear button after removal', () => {
+        manager.toggleClearSearchButton();
+
+        const clearButton = document.querySelector('.clear-search');
+
+        expect(clearButton.classList.contains('hidden')).toEqual(false);
+        expect(getVisualTokens().length).toEqual(1);
+
+        dispatchBackspaceEvent(document, 'keydown');
+
+        expect(clearButton.classList.contains('hidden')).toEqual(true);
+        expect(getVisualTokens().length).toEqual(0);
+      });
+    });
+
+    describe('unselects token', () => {
+      beforeEach(() => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+          ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+        `);
+      });
+
+      it('unselects token when input is clicked', () => {
+        const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+        expect(selectedToken.classList.contains('selected')).toEqual(true);
+        expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+        // Click directly on input attached to document
+        // so that the click event will propagate properly
+        document.querySelector('.filtered-search').click();
+
+        expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+        expect(selectedToken.classList.contains('selected')).toEqual(false);
+      });
+
+      it('unselects token when document.body is clicked', () => {
+        const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+        expect(selectedToken.classList.contains('selected')).toEqual(true);
+        expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+        document.body.click();
+
+        expect(selectedToken.classList.contains('selected')).toEqual(false);
+        expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+      });
+    });
+
+    describe('toggleInputContainerFocus', () => {
+      it('toggles on focus', () => {
+        input.focus();
+        expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true);
+      });
+
+      it('toggles on blur', () => {
+        input.blur();
+        expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false);
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
deleted file mode 100644
index 98959dda2423a585932b84c8708232a6644a147a..0000000000000000000000000000000000000000
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ /dev/null
@@ -1,67 +0,0 @@
-require('~/lib/utils/url_utility');
-require('~/lib/utils/common_utils');
-require('~/filtered_search/filtered_search_token_keys');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown_manager');
-require('~/filtered_search/filtered_search_manager');
-
-(() => {
-  describe('Filtered Search Manager', () => {
-    describe('search', () => {
-      let manager;
-      const defaultParams = '?scope=all&utf8=✓&state=opened';
-
-      function getInput() {
-        return document.querySelector('.filtered-search');
-      }
-
-      beforeEach(() => {
-        setFixtures(`
-          <input type='text' class='filtered-search' />
-        `);
-
-        spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
-        spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
-        spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
-        spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
-        spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
-
-        manager = new gl.FilteredSearchManager();
-      });
-
-      afterEach(() => {
-        getInput().outerHTML = '';
-      });
-
-      it('should search with a single word', () => {
-        getInput().value = 'searchTerm';
-
-        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
-          expect(url).toEqual(`${defaultParams}&search=searchTerm`);
-        });
-
-        manager.search();
-      });
-
-      it('should search with multiple words', () => {
-        getInput().value = 'awesome search terms';
-
-        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
-          expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
-        });
-
-        manager.search();
-      });
-
-      it('should search with special characters', () => {
-        getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
-
-        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
-          expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
-        });
-
-        manager.search();
-      });
-    });
-  });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
similarity index 100%
rename from spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
rename to spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
similarity index 100%
rename from spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
rename to spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bbda1476fed621f542ad42bf2f7373790a6ab6b1
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -0,0 +1,600 @@
+require('~/filtered_search/filtered_search_visual_tokens');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
+
+describe('Filtered Search Visual Tokens', () => {
+  let tokensContainer;
+
+  beforeEach(() => {
+    setFixtures(`
+      <ul class="tokens-container">
+        ${FilteredSearchSpecHelper.createInputHTML()}
+      </ul>
+    `);
+    tokensContainer = document.querySelector('.tokens-container');
+  });
+
+  describe('getLastVisualTokenBeforeInput', () => {
+    it('returns when there are no visual tokens', () => {
+      const { lastVisualToken, isLastVisualTokenValid }
+        = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+      expect(lastVisualToken).toEqual(null);
+      expect(isLastVisualTokenValid).toEqual(true);
+    });
+
+    describe('input is the last item in tokensContainer', () => {
+      it('returns when there is one visual token', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+          FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+        );
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+        expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+        expect(isLastVisualTokenValid).toEqual(true);
+      });
+
+      it('returns when there is an incomplete visual token', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+          FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'),
+        );
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+        expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+        expect(isLastVisualTokenValid).toEqual(false);
+      });
+
+      it('returns when there are multiple visual tokens', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+          ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+        `);
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+        const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+        expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+        expect(isLastVisualTokenValid).toEqual(true);
+      });
+
+      it('returns when there are multiple visual tokens and an incomplete visual token', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+          ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+          ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
+        `);
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+        const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+        expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+        expect(isLastVisualTokenValid).toEqual(false);
+      });
+    });
+
+    describe('input is a middle item in tokensContainer', () => {
+      it('returns last token before input', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+          ${FilteredSearchSpecHelper.createInputHTML()}
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+        `);
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+        expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+        expect(isLastVisualTokenValid).toEqual(true);
+      });
+
+      it('returns last partial token before input', () => {
+        tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+          ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+          ${FilteredSearchSpecHelper.createInputHTML()}
+          ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+        `);
+
+        const { lastVisualToken, isLastVisualTokenValid }
+          = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+        expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+        expect(isLastVisualTokenValid).toEqual(false);
+      });
+    });
+  });
+
+  describe('unselectTokens', () => {
+    it('does nothing when there are no tokens', () => {
+      const beforeHTML = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.unselectTokens();
+
+      expect(tokensContainer.innerHTML).toEqual(beforeHTML);
+    });
+
+    it('removes the selected class from buttons', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
+      `);
+
+      const selected = tokensContainer.querySelector('.js-visual-token .selected');
+      expect(selected.classList.contains('selected')).toEqual(true);
+
+      gl.FilteredSearchVisualTokens.unselectTokens();
+
+      expect(selected.classList.contains('selected')).toEqual(false);
+    });
+  });
+
+  describe('selectToken', () => {
+    beforeEach(() => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+        ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+      `);
+    });
+
+    it('removes the selected class if it has selected class', () => {
+      const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+      firstTokenButton.classList.add('selected');
+
+      gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+      expect(firstTokenButton.classList.contains('selected')).toEqual(false);
+    });
+
+    describe('has no selected class', () => {
+      it('adds selected class', () => {
+        const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+
+        gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+        expect(firstTokenButton.classList.contains('selected')).toEqual(true);
+      });
+
+      it('removes selected class from other tokens', () => {
+        const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
+        tokenButtons[1].classList.add('selected');
+
+        gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+
+        expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
+        expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
+      });
+    });
+  });
+
+  describe('removeSelectedToken', () => {
+    it('does not remove when there are no selected tokens', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
+      );
+
+      expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+      gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+      expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+    });
+
+    it('removes selected token', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+      );
+
+      expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+      gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+      expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
+    });
+  });
+
+  describe('createVisualTokenElementHTML', () => {
+    let tokenElement;
+
+    beforeEach(() => {
+      setFixtures(`
+        <div class="test-area">
+        ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+        </div>
+      `);
+
+      tokenElement = document.querySelector('.test-area').firstElementChild;
+    });
+
+    it('contains name div', () => {
+      expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
+    });
+
+    it('contains value div', () => {
+      expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+    });
+
+    it('contains selectable class', () => {
+      expect(tokenElement.classList.contains('selectable')).toEqual(true);
+    });
+
+    it('contains button role', () => {
+      expect(tokenElement.getAttribute('role')).toEqual('button');
+    });
+  });
+
+  describe('addVisualTokenElement', () => {
+    it('renders search visual tokens', () => {
+      gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-term')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('search term');
+      expect(token.querySelector('.value')).toEqual(null);
+    });
+
+    it('renders filter visual token name', () => {
+      gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-token')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('milestone');
+      expect(token.querySelector('.value')).toEqual(null);
+    });
+
+    it('renders filter visual token name and value', () => {
+      gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-token')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('label');
+      expect(token.querySelector('.value').innerText).toEqual('Frontend');
+    });
+
+    it('inserts visual token before input', () => {
+      tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
+
+      gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+      const tokens = tokensContainer.querySelectorAll('.js-visual-token');
+      const labelToken = tokens[0];
+      const assigneeToken = tokens[1];
+
+      expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
+      expect(labelToken.querySelector('.name').innerText).toEqual('label');
+      expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+
+      expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
+      expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
+      expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
+    });
+  });
+
+  describe('addValueToPreviousVisualTokenElement', () => {
+    it('does not add when previous visual token element has no value', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'),
+      );
+
+      const original = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+      expect(original).toEqual(tokensContainer.innerHTML);
+    });
+
+    it('does not add when previous visual token element is a search', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+        ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+      `);
+
+      const original = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+      expect(original).toEqual(tokensContainer.innerHTML);
+    });
+
+    it('adds value to previous visual filter token', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'),
+      );
+
+      const original = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+      const updatedToken = tokensContainer.querySelector('.js-visual-token');
+
+      expect(updatedToken.querySelector('.name').innerText).toEqual('label');
+      expect(updatedToken.querySelector('.value').innerText).toEqual('value');
+      expect(original).not.toEqual(tokensContainer.innerHTML);
+    });
+  });
+
+  describe('addFilterVisualToken', () => {
+    it('creates visual token with just tokenName', () => {
+      gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-token')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('milestone');
+      expect(token.querySelector('.value')).toEqual(null);
+    });
+
+    it('creates visual token with just tokenValue', () => {
+      gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+      gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-token')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('milestone');
+      expect(token.querySelector('.value').innerText).toEqual('%8.17');
+    });
+
+    it('creates full visual token', () => {
+      gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-token')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('assignee');
+      expect(token.querySelector('.value').innerText).toEqual('@john');
+    });
+  });
+
+  describe('addSearchVisualToken', () => {
+    it('creates search visual token', () => {
+      gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
+      const token = tokensContainer.querySelector('.js-visual-token');
+
+      expect(token.classList.contains('filtered-search-term')).toEqual(true);
+      expect(token.querySelector('.name').innerText).toEqual('search term');
+      expect(token.querySelector('.value')).toEqual(null);
+    });
+
+    it('appends to previous search visual token if previous token was a search token', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+        ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+      `);
+
+      gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+      const token = tokensContainer.querySelector('.filtered-search-term');
+
+      expect(token.querySelector('.name').innerText).toEqual('search term append this');
+      expect(token.querySelector('.value')).toEqual(null);
+    });
+  });
+
+  describe('getLastTokenPartial', () => {
+    it('should get last token value', () => {
+      const value = '~bug';
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+      );
+
+      expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
+    });
+
+    it('should get last token name if there is no value', () => {
+      const name = 'assignee';
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
+      );
+
+      expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
+    });
+
+    it('should return empty when there are no tokens', () => {
+      expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
+    });
+  });
+
+  describe('removeLastTokenPartial', () => {
+    it('should remove the last token value if it exists', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'),
+      );
+
+      expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
+
+      gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+      expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
+    });
+
+    it('should remove the last token name if there is no value', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'),
+      );
+
+      expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
+
+      gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+      expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
+    });
+
+    it('should not remove anything when there are no tokens', () => {
+      const html = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+      expect(tokensContainer.innerHTML).toEqual(html);
+    });
+  });
+
+  describe('tokenizeInput', () => {
+    it('does not do anything if there is no input', () => {
+      const original = tokensContainer.innerHTML;
+      gl.FilteredSearchVisualTokens.tokenizeInput();
+
+      expect(tokensContainer.innerHTML).toEqual(original);
+    });
+
+    it('adds search visual token if previous visual token is valid', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'),
+      );
+
+      const input = document.querySelector('.filtered-search');
+      input.value = 'some value';
+      gl.FilteredSearchVisualTokens.tokenizeInput();
+
+      const newToken = tokensContainer.querySelector('.filtered-search-term');
+
+      expect(input.value).toEqual('');
+      expect(newToken.querySelector('.name').innerText).toEqual('some value');
+      expect(newToken.querySelector('.value')).toEqual(null);
+    });
+
+    it('adds value to previous visual token element if previous visual token is invalid', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'),
+      );
+
+      const input = document.querySelector('.filtered-search');
+      input.value = '@john';
+      gl.FilteredSearchVisualTokens.tokenizeInput();
+
+      const updatedToken = tokensContainer.querySelector('.filtered-search-token');
+
+      expect(input.value).toEqual('');
+      expect(updatedToken.querySelector('.name').innerText).toEqual('assignee');
+      expect(updatedToken.querySelector('.value').innerText).toEqual('@john');
+    });
+  });
+
+  describe('editToken', () => {
+    let input;
+    let token;
+
+    beforeEach(() => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+        ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+      `);
+
+      input = document.querySelector('.filtered-search');
+      token = document.querySelector('.js-visual-token');
+    });
+
+    it('tokenize\'s existing input', () => {
+      input.value = 'some text';
+      spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+
+      gl.FilteredSearchVisualTokens.editToken(token);
+
+      expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+      expect(input.value).not.toEqual('some text');
+    });
+
+    it('moves input to the token position', () => {
+      expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
+
+      gl.FilteredSearchVisualTokens.editToken(token);
+
+      expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
+      expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
+    });
+
+    it('input contains the visual token value', () => {
+      gl.FilteredSearchVisualTokens.editToken(token);
+
+      expect(input.value).toEqual('none');
+    });
+
+    describe('selected token is a search term token', () => {
+      beforeEach(() => {
+        token = document.querySelector('.filtered-search-term');
+      });
+
+      it('token is removed', () => {
+        expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
+
+        gl.FilteredSearchVisualTokens.editToken(token);
+
+        expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
+      });
+
+      it('input has the same value as removed token', () => {
+        expect(input.value).toEqual('');
+
+        gl.FilteredSearchVisualTokens.editToken(token);
+
+        expect(input.value).toEqual('search');
+      });
+    });
+  });
+
+  describe('moveInputTotheRight', () => {
+    it('does nothing if the input is already the right most element', () => {
+      tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+        FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
+      );
+
+      spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {});
+      spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+
+      gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+      expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+      expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+    });
+
+    it('tokenize\'s input', () => {
+      tokensContainer.innerHTML = `
+        ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+        ${FilteredSearchSpecHelper.createInputHTML()}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+      `;
+
+      document.querySelector('.filtered-search').value = 'none';
+
+      gl.FilteredSearchVisualTokens.moveInputToTheRight();
+      const value = tokensContainer.querySelector('.js-visual-token .value');
+
+      expect(value.innerText).toEqual('none');
+    });
+
+    it('converts input into search term token if last token is valid', () => {
+      tokensContainer.innerHTML = `
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+        ${FilteredSearchSpecHelper.createInputHTML()}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+      `;
+
+      document.querySelector('.filtered-search').value = 'test';
+
+      gl.FilteredSearchVisualTokens.moveInputToTheRight();
+      const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
+
+      expect(searchValue.innerText).toEqual('test');
+    });
+
+    it('moves the input to the right most element', () => {
+      tokensContainer.innerHTML = `
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+        ${FilteredSearchSpecHelper.createInputHTML()}
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+      `;
+
+      gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+      expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
+    });
+
+    it('tokenizes input even if input is the right most element', () => {
+      tokensContainer.innerHTML = `
+        ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+        ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+        ${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
+      `;
+
+      gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+      const token = tokensContainer.children[1];
+      expect(token.querySelector('.value').innerText).toEqual('~bug');
+    });
+  });
+});
diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..09d8c9df3b2771ba3dc0804c5b050096345bc597
--- /dev/null
+++ b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
@@ -0,0 +1,2 @@
+%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}}
+  %i.fa.fa-trash-o
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index 0e7c2351b6668a3b14248a2573166fb4cd9405ed..a059818145b9b44de9a68f58cbc61f4e46194a0f 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -20,7 +20,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
   it 'branches/new_branch.html.raw' do |example|
     get :new,
       namespace_id: project.namespace.to_param,
-      project_id: project.to_param
+      project_id: project
 
     expect(response).to be_success
     store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb
index 978e25a1c32f46cc3130c76da93696ef674d665a..320de791b08654fb25ddbb131c8f1224285ffa3e 100644
--- a/spec/javascripts/fixtures/builds.rb
+++ b/spec/javascripts/fixtures/builds.rb
@@ -24,7 +24,7 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller
   it 'builds/build-with-artifacts.html.raw' do |example|
     get :show,
       namespace_id: project.namespace.to_param,
-      project_id: project.to_param,
+      project_id: project,
       id: build_with_artifacts.to_param
 
     expect(response).to be_success
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
deleted file mode 100644
index a50812d9517bc3c2cdad724f925d0cb788289deb..0000000000000000000000000000000000000000
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable space-before-function-paren */
-(function() {
-  window.emojiMenu = "<div class='emoji-menu'>\n  <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n  <div class='emoji-menu-content'>\n    <h5 class='emoji-menu-title'>\n    Emoticons\n    </h5>\n    <ul class='clearfix emoji-menu-list'>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n        </button>\n      </li>\n      <li class='pull-left text-center emoji-menu-list-item'>\n        <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n        <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n        </button>\n      </li>\n    </ul>\n  </div>\n</div>";
-}).call(window);
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..483063fb889fec7e736d709223b92f623aa2a708
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/metrics.html.haml
@@ -0,0 +1,12 @@
+%div
+  .top-area
+    .row
+      .col-sm-6
+        %h3.page-title
+          Metrics for environment
+  .row
+    .col-sm-12
+      %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+  .row
+    .col-sm-12
+      %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
\ No newline at end of file
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 06f708f9e151a53a4bee1803762e8739d4da6a93..88e3f86080984e097f329efc6a0a52ecd2e6c4d6 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -41,7 +41,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
   def render_issue(fixture_file_name, issue)
     get :show,
       namespace_id: project.namespace.to_param,
-      project_id: project.to_param,
+      project_id: project,
       id: issue.to_param
 
     expect(response).to be_success
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 62984097099f8313b2a2a23940fb04af68293b5f..ee893b76c84405357fd1d099592deebd0f794503 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -27,7 +27,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
   def render_merge_request(fixture_file_name, merge_request)
     get :show,
       namespace_id: project.namespace.to_param,
-      project_id: project.to_param,
+      project_id: project,
       id: merge_request.to_param
 
     expect(response).to be_success
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
new file mode 100644
index 0000000000000000000000000000000000000000..a96a4c0c09529dce3023c46a5dc6e436e34cdb0e
--- /dev/null
+++ b/spec/javascripts/fixtures/project_branches.json
@@ -0,0 +1,5 @@
+[
+  "master",
+  "development",
+  "staging"
+]
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 4ce7f5c601aab978c8c2fe0ee1c0b686a9a40966..1339ee008704ea76921295aaf18a4674146df738 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -43,7 +43,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 0,
   "permissions": {
     "project_access": null,
@@ -88,7 +88,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 5,
   "permissions": {
     "project_access": {
@@ -139,7 +139,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": true,
+  "only_allow_merge_if_pipeline_succeeds": true,
   "open_issues_count": 4,
   "permissions": {
     "project_access": null,
@@ -187,7 +187,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": true,
+  "only_allow_merge_if_pipeline_succeeds": true,
   "open_issues_count": 4,
   "permissions": {
     "project_access": null,
@@ -235,7 +235,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 5,
   "permissions": {
     "project_access": null,
@@ -283,7 +283,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 5,
   "permissions": {
     "project_access": {
@@ -334,7 +334,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 3,
   "permissions": {
     "project_access": null,
@@ -382,7 +382,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 5,
   "permissions": {
     "project_access": {
@@ -433,7 +433,7 @@
   "avatar_url": null,
   "star_count": 0,
   "forks_count": 0,
-  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_pipeline_succeeds": false,
   "open_issues_count": 5,
   "permissions": {
     "project_access": null,
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 56513219e1ec52fb2d8a83732b996cff54e99f01..6c33b240e5cb086dbb95671379f125e24f1716c0 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -20,7 +20,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
   it 'projects/dashboard.html.raw' do |example|
     get :show,
       namespace_id: project.namespace.to_param,
-      id: project.to_param
+      id: project
 
     expect(response).to be_success
     store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..821fb7940a0e4d252d14ad64489d88add8452ab2
--- /dev/null
+++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
@@ -0,0 +1,28 @@
+%form.js-edit-blob-form
+  %input{type: 'hidden', name: 'target_branch', value: 'master'}
+  %div
+    .dropdown
+      %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
+      .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
+        .dropdown-page-one
+          .dropdown-title 'Select branch'
+          .dropdown-input
+            %input.dropdown-input-field{type: 'search', value: ''}
+            %i.fa.fa-search.dropdown-input-search
+            %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
+          .dropdown-content
+          .dropdown-footer
+            %ul.dropdown-footer-list
+              %li
+                %a.create-new-branch.dropdown-toggle-page{href: "#"}
+                  Create new branch
+        .dropdown-page-two.dropdown-new-branch
+          %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
+          .dropdown_title 'Create new branch'
+          .dropdown_content
+            %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
+              %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+                Create
+              %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+                Cancel
+  %button{type: 'submit'}
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
index 2c08b06ea9e856c5288521c4e98a7df7d9c7600e..a81ef8c5492ce4744f8a80f492bee69bca6fd802 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -39,8 +39,8 @@ describe 'Todos (JavaScript fixtures)' do
 
     it 'todos/todos.json' do |example|
       post :create,
-        namespace_id: namespace.path,
-        project_id: project.path,
+        namespace_id: namespace,
+        project_id: project,
         issuable_type: 'issue',
         issuable_id: issue_2.id,
         format: 'json'
diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..275359bde0ae105156923762ea3066dccc18fd90
--- /dev/null
+++ b/spec/javascripts/fixtures/user_callout.html.haml
@@ -0,0 +1,2 @@
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js
similarity index 100%
rename from spec/javascripts/gfm_auto_complete_spec.js.es6
rename to spec/javascripts/gfm_auto_complete_spec.js
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js
similarity index 100%
rename from spec/javascripts/gl_dropdown_spec.js.es6
rename to spec/javascripts/gl_dropdown_spec.js
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2b46640e5ba7cf8f3273b2485e9a4da1eaddbb3
--- /dev/null
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -0,0 +1,399 @@
+import { glEmojiTag } from '~/behaviors/gl_emoji';
+import {
+  isEmojiUnicodeSupported,
+  isFlagEmoji,
+  isKeycapEmoji,
+  isSkinToneComboEmoji,
+  isHorceRacingSkinToneComboEmoji,
+  isPersonZwjEmoji,
+} from '~/behaviors/gl_emoji/is_emoji_unicode_supported';
+
+const emptySupportMap = {
+  personZwj: false,
+  horseRacing: false,
+  flag: false,
+  skinToneModifier: false,
+  '9.0': false,
+  '8.0': false,
+  '7.0': false,
+  6.1: false,
+  '6.0': false,
+  5.2: false,
+  5.1: false,
+  4.1: false,
+  '4.0': false,
+  3.2: false,
+  '3.0': false,
+  1.1: false,
+};
+
+const emojiFixtureMap = {
+  bomb: {
+    name: 'bomb',
+    moji: '💣',
+    unicodeVersion: '6.0',
+  },
+  construction_worker_tone5: {
+    name: 'construction_worker_tone5',
+    moji: '👷🏿',
+    unicodeVersion: '8.0',
+  },
+  five: {
+    name: 'five',
+    moji: '5️⃣',
+    unicodeVersion: '3.0',
+  },
+  grey_question: {
+    name: 'grey_question',
+    moji: '❔',
+    unicodeVersion: '6.0',
+  },
+};
+
+function markupToDomElement(markup) {
+  const div = document.createElement('div');
+  div.innerHTML = markup;
+  return div.firstElementChild;
+}
+
+function testGlEmojiImageFallback(element, name, src) {
+  expect(element.tagName.toLowerCase()).toBe('img');
+  expect(element.getAttribute('src')).toBe(src);
+  expect(element.getAttribute('title')).toBe(`:${name}:`);
+  expect(element.getAttribute('alt')).toBe(`:${name}:`);
+}
+
+const defaults = {
+  forceFallback: false,
+  sprite: false,
+};
+
+function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
+  const opts = Object.assign({}, defaults, options);
+  expect(element.tagName.toLowerCase()).toBe('gl-emoji');
+  expect(element.dataset.name).toBe(name);
+  expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
+  expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
+
+  const fallbackSpriteClass = `emoji-${name}`;
+  if (opts.sprite) {
+    expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
+  }
+
+  if (opts.forceFallback && opts.sprite) {
+    expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
+  }
+
+  if (opts.forceFallback && !opts.sprite) {
+    // Check for image fallback
+    testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
+  } else {
+    // Otherwise make sure things are still unicode text
+    expect(element.textContent.trim()).toBe(unicodeMoji);
+  }
+}
+
+describe('gl_emoji', () => {
+  describe('glEmojiTag', () => {
+    it('bomb emoji', () => {
+      const emojiKey = 'bomb';
+      const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+      );
+    });
+
+    it('bomb emoji with image fallback', () => {
+      const emojiKey = 'bomb';
+      const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+        forceFallback: true,
+      });
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+        {
+          forceFallback: true,
+        },
+      );
+    });
+
+    it('bomb emoji with sprite fallback readiness', () => {
+      const emojiKey = 'bomb';
+      const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+        sprite: true,
+      });
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+        {
+          sprite: true,
+        },
+      );
+    });
+    it('bomb emoji with sprite fallback', () => {
+      const emojiKey = 'bomb';
+      const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+        forceFallback: true,
+        sprite: true,
+      });
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+        {
+          forceFallback: true,
+          sprite: true,
+        },
+      );
+    });
+
+    it('question mark when invalid emoji name given', () => {
+      const name = 'invalid_emoji';
+      const emojiKey = 'grey_question';
+      const markup = glEmojiTag(name);
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+      );
+    });
+
+    it('question mark with image fallback when invalid emoji name given', () => {
+      const name = 'invalid_emoji';
+      const emojiKey = 'grey_question';
+      const markup = glEmojiTag(name, {
+        forceFallback: true,
+      });
+      const glEmojiElement = markupToDomElement(markup);
+      testGlEmojiElement(
+        glEmojiElement,
+        emojiFixtureMap[emojiKey].name,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+        emojiFixtureMap[emojiKey].moji,
+        {
+          forceFallback: true,
+        },
+      );
+    });
+  });
+
+  describe('isFlagEmoji', () => {
+    it('should detect flag_ac', () => {
+      expect(isFlagEmoji('🇦🇨')).toBeTruthy();
+    });
+    it('should detect flag_us', () => {
+      expect(isFlagEmoji('🇺🇸')).toBeTruthy();
+    });
+    it('should detect flag_zw', () => {
+      expect(isFlagEmoji('🇿🇼')).toBeTruthy();
+    });
+    it('should not detect flags', () => {
+      expect(isFlagEmoji('🎏')).toBeFalsy();
+    });
+    it('should not detect triangular_flag_on_post', () => {
+      expect(isFlagEmoji('🚩')).toBeFalsy();
+    });
+    it('should not detect single letter', () => {
+      expect(isFlagEmoji('🇦')).toBeFalsy();
+    });
+    it('should not detect >2 letters', () => {
+      expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
+    });
+  });
+
+  describe('isKeycapEmoji', () => {
+    it('should detect one(keycap)', () => {
+      expect(isKeycapEmoji('1️⃣')).toBeTruthy();
+    });
+    it('should detect nine(keycap)', () => {
+      expect(isKeycapEmoji('9️⃣')).toBeTruthy();
+    });
+    it('should not detect ten(keycap)', () => {
+      expect(isKeycapEmoji('🔟')).toBeFalsy();
+    });
+    it('should not detect hash(keycap)', () => {
+      expect(isKeycapEmoji('#⃣')).toBeFalsy();
+    });
+  });
+
+  describe('isSkinToneComboEmoji', () => {
+    it('should detect hand_splayed_tone5', () => {
+      expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy();
+    });
+    it('should not detect hand_splayed', () => {
+      expect(isSkinToneComboEmoji('🖐')).toBeFalsy();
+    });
+    it('should detect lifter_tone1', () => {
+      expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy();
+    });
+    it('should not detect lifter', () => {
+      expect(isSkinToneComboEmoji('🏋')).toBeFalsy();
+    });
+    it('should detect rowboat_tone4', () => {
+      expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy();
+    });
+    it('should not detect rowboat', () => {
+      expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
+    });
+    it('should not detect individual tone emoji', () => {
+      expect(isSkinToneComboEmoji('🏻')).toBeFalsy();
+    });
+  });
+
+  describe('isHorceRacingSkinToneComboEmoji', () => {
+    it('should detect horse_racing_tone2', () => {
+      expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy();
+    });
+    it('should not detect horse_racing', () => {
+      expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy();
+    });
+  });
+
+  describe('isPersonZwjEmoji', () => {
+    it('should detect couple_mm', () => {
+      expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy();
+    });
+    it('should not detect couple_with_heart', () => {
+      expect(isPersonZwjEmoji('💑')).toBeFalsy();
+    });
+    it('should not detect couplekiss', () => {
+      expect(isPersonZwjEmoji('💏')).toBeFalsy();
+    });
+    it('should detect family_mmb', () => {
+      expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy();
+    });
+    it('should detect family_mwgb', () => {
+      expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy();
+    });
+    it('should not detect family', () => {
+      expect(isPersonZwjEmoji('👪')).toBeFalsy();
+    });
+    it('should detect kiss_ww', () => {
+      expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy();
+    });
+    it('should not detect girl', () => {
+      expect(isPersonZwjEmoji('👧')).toBeFalsy();
+    });
+    it('should not detect girl_tone5', () => {
+      expect(isPersonZwjEmoji('👧🏿')).toBeFalsy();
+    });
+    it('should not detect man', () => {
+      expect(isPersonZwjEmoji('👨')).toBeFalsy();
+    });
+    it('should not detect woman', () => {
+      expect(isPersonZwjEmoji('👩')).toBeFalsy();
+    });
+  });
+
+  describe('isEmojiUnicodeSupported', () => {
+    it('bomb(6.0) with 6.0 support', () => {
+      const emojiKey = 'bomb';
+      const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+        '6.0': true,
+      });
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeTruthy();
+    });
+
+    it('bomb(6.0) without 6.0 support', () => {
+      const emojiKey = 'bomb';
+      const unicodeSupportMap = emptySupportMap;
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeFalsy();
+    });
+
+    it('bomb(6.0) without 6.0 but with 9.0 support', () => {
+      const emojiKey = 'bomb';
+      const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+        '9.0': true,
+      });
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeFalsy();
+    });
+
+    it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
+      const emojiKey = 'construction_worker_tone5';
+      const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+        skinToneModifier: false,
+        '9.0': true,
+        '8.0': true,
+        '7.0': true,
+        6.1: true,
+        '6.0': true,
+        5.2: true,
+        5.1: true,
+        4.1: true,
+        '4.0': true,
+        3.2: true,
+        '3.0': true,
+        1.1: true,
+      });
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeFalsy();
+    });
+
+    it('use native keycap on >=57 chrome', () => {
+      const emojiKey = 'five';
+      const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+        '3.0': true,
+        meta: {
+          isChrome: true,
+          chromeVersion: 57,
+        },
+      });
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeTruthy();
+    });
+
+    it('fallback keycap on <57 chrome', () => {
+      const emojiKey = 'five';
+      const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+        '3.0': true,
+        meta: {
+          isChrome: true,
+          chromeVersion: 50,
+        },
+      });
+      const isSupported = isEmojiUnicodeSupported(
+        unicodeSupportMap,
+        emojiFixtureMap[emojiKey].moji,
+        emojiFixtureMap[emojiKey].unicodeVersion,
+      );
+      expect(isSupported).toBeFalsy();
+    });
+  });
+});
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js
similarity index 100%
rename from spec/javascripts/gl_field_errors_spec.js.es6
rename to spec/javascripts/gl_field_errors_spec.js
diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js
similarity index 100%
rename from spec/javascripts/gl_form_spec.js.es6
rename to spec/javascripts/gl_form_spec.js
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index a954bb60560e8253a905cf3b6dc808bf5efb5f4d..861f26e162ff1c7a254209561dc772c8cdb72c30 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,9 +1,7 @@
-/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */
-/* global d3 */
-/* global ContributorsGraph */
-/* global ContributorsMasterGraph */
+/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */
 
-require('~/graphs/stat_graph_contributors_graph');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph';
 
 describe("ContributorsGraph", function () {
   describe("#set_x_domain", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index b15764abe8c3ca908fbe690b2d88ef482dd4c36c..9b47ab6218170f2703e77246985c8bcafb77f578 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,7 +1,6 @@
 /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */
-/* global ContributorsStatGraphUtil */
 
-require('~/graphs/stat_graph_contributors_util');
+import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util';
 
 describe("ContributorsStatGraphUtil", function () {
   describe("#parse_log", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
deleted file mode 100644
index 876c23361bc79060b84d22743e4ec6df446ed928..0000000000000000000000000000000000000000
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* eslint-disable quotes */
-/* global StatGraph */
-
-require('~/graphs/stat_graph');
-
-describe("StatGraph", function () {
-  describe("#get_log", function () {
-    it("returns log", function () {
-      StatGraph.log = "test";
-      expect(StatGraph.get_log()).toBe("test");
-    });
-  });
-
-  describe("#set_log", function () {
-    it("sets the log", function () {
-      StatGraph.set_log("test");
-      expect(StatGraph.log).toBe("test");
-    });
-  });
-});
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js
similarity index 88%
rename from spec/javascripts/helpers/class_spec_helper.js.es6
rename to spec/javascripts/helpers/class_spec_helper.js
index d3c37d39431950cfd4d8531ba791a5afadaba9eb..61db27a8fccb0d90a14898d91c4ad1af8e22ab96 100644
--- a/spec/javascripts/helpers/class_spec_helper.js.es6
+++ b/spec/javascripts/helpers/class_spec_helper.js
@@ -7,3 +7,5 @@ class ClassSpecHelper {
 }
 
 window.ClassSpecHelper = ClassSpecHelper;
+
+module.exports = ClassSpecHelper;
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js
similarity index 100%
rename from spec/javascripts/helpers/class_spec_helper_spec.js.es6
rename to spec/javascripts/helpers/class_spec_helper_spec.js
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce83a256dddbcbe435d99d2aa5f0063de8b8af64
--- /dev/null
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -0,0 +1,52 @@
+class FilteredSearchSpecHelper {
+  static createFilterVisualTokenHTML(name, value, isSelected) {
+    return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML;
+  }
+
+  static createFilterVisualToken(name, value, isSelected = false) {
+    const li = document.createElement('li');
+    li.classList.add('js-visual-token', 'filtered-search-token');
+
+    li.innerHTML = `
+      <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+        <div class="name">${name}</div>
+        <div class="value">${value}</div>
+      </div>
+    `;
+
+    return li;
+  }
+
+  static createNameFilterVisualTokenHTML(name) {
+    return `
+      <li class="js-visual-token filtered-search-token">
+        <div class="name">${name}</div>
+      </li>
+    `;
+  }
+
+  static createSearchVisualTokenHTML(name) {
+    return `
+      <li class="js-visual-token filtered-search-term">
+        <div class="name">${name}</div>
+      </li>
+    `;
+  }
+
+  static createInputHTML(placeholder = '', value = '') {
+    return `
+      <li class="input-token">
+        <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/>
+      </li>
+    `;
+  }
+
+  static createTokensContainerHTML(html, inputPlaceholder) {
+    return `
+      ${html}
+      ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
+    `;
+  }
+}
+
+module.exports = FilteredSearchSpecHelper;
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js
similarity index 100%
rename from spec/javascripts/issuable_spec.js.es6
rename to spec/javascripts/issuable_spec.js
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js
similarity index 100%
rename from spec/javascripts/issuable_time_tracker_spec.js.es6
rename to spec/javascripts/issuable_time_tracker_spec.js
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index e7530f6138593e2e9c84c3142bd3984782c15587..aabc8bea12f48f7001f7dec9143edb443b342e80 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,10 +1,9 @@
 /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
-/* global Issue */
+import Issue from '~/issue';
 
 require('~/lib/utils/text_utility');
-require('~/issue');
 
-(function() {
+describe('Issue', function() {
   var INVALID_URL = 'http://goesnowhere.nothing/whereami';
   var $boxClosed, $boxOpen, $btnClose, $btnReopen;
 
@@ -59,28 +58,26 @@ require('~/issue');
     expect($btnReopen).toHaveText('Reopen issue');
   }
 
-  describe('Issue', function() {
-    describe('task lists', function() {
-      beforeEach(function() {
-        loadFixtures('issues/issue-with-task-list.html.raw');
-        this.issue = new Issue();
-      });
-
-      it('modifies the Markdown field', function() {
-        spyOn(jQuery, 'ajax').and.stub();
-        $('input[type=checkbox]').attr('checked', true).trigger('change');
-        expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
-      });
+  describe('task lists', function() {
+    beforeEach(function() {
+      loadFixtures('issues/issue-with-task-list.html.raw');
+      this.issue = new Issue();
+    });
 
-      it('submits an ajax request on tasklist:changed', function() {
-        spyOn(jQuery, 'ajax').and.callFake(function(req) {
-          expect(req.type).toBe('PATCH');
-          expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
-          expect(req.data.issue.description).not.toBe(null);
-        });
+    it('modifies the Markdown field', function() {
+      spyOn(jQuery, 'ajax').and.stub();
+      $('input[type=checkbox]').attr('checked', true).trigger('change');
+      expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+    });
 
-        $('.js-task-list-field').trigger('tasklist:changed');
+    it('submits an ajax request on tasklist:changed', function() {
+      spyOn(jQuery, 'ajax').and.callFake(function(req) {
+        expect(req.type).toBe('PATCH');
+        expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
+        expect(req.data.issue.description).not.toBe(null);
       });
+
+      $('.js-task-list-field').trigger('tasklist:changed');
     });
   });
 
@@ -139,6 +136,21 @@ require('~/issue');
       expectErrorMessage();
       expect($('.issue_counter')).toHaveText(1);
     });
+
+    it('updates counter', () => {
+      spyOn(jQuery, 'ajax').and.callFake(function(req) {
+        expectPendingRequest(req, $btnClose);
+        req.success({
+          id: 34
+        });
+      });
+
+      expect($('.issue_counter')).toHaveText(1);
+      $('.issue_counter').text('1,001');
+      expect($('.issue_counter').text()).toEqual('1,001');
+      $btnClose.trigger('click');
+      expect($('.issue_counter').text()).toEqual('1,000');
+    });
   });
 
   describe('reopen issue', function() {
@@ -165,4 +177,4 @@ require('~/issue');
       expect($('.issue_counter')).toHaveText(1);
     });
   });
-}).call(window);
+});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js
similarity index 100%
rename from spec/javascripts/labels_issue_sidebar_spec.js.es6
rename to spec/javascripts/labels_issue_sidebar_spec.js
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js
similarity index 100%
rename from spec/javascripts/lib/utils/common_utils_spec.js.es6
rename to spec/javascripts/lib/utils/common_utils_spec.js
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4200e9431219cef3dcc030e73a41852b5e092fcb
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -0,0 +1,110 @@
+require('~/lib/utils/text_utility');
+
+(() => {
+  describe('text_utility', () => {
+    describe('gl.text.getTextWidth', () => {
+      it('returns zero width when no text is passed', () => {
+        expect(gl.text.getTextWidth('')).toBe(0);
+      });
+
+      it('returns zero width when no text is passed and font is passed', () => {
+        expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+      });
+
+      it('returns width when text is passed', () => {
+        expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+      });
+
+      it('returns bigger width when font is larger', () => {
+        const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+        const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+        expect(largeFont > regular).toBe(true);
+      });
+    });
+
+    describe('gl.text.pluralize', () => {
+      it('returns pluralized', () => {
+        expect(gl.text.pluralize('test', 2)).toBe('tests');
+      });
+
+      it('returns pluralized when count is 0', () => {
+        expect(gl.text.pluralize('test', 0)).toBe('tests');
+      });
+
+      it('does not return pluralized', () => {
+        expect(gl.text.pluralize('test', 1)).toBe('test');
+      });
+    });
+
+    describe('gl.text.highCountTrim', () => {
+      it('returns 99+ for count >= 100', () => {
+        expect(gl.text.highCountTrim(105)).toBe('99+');
+        expect(gl.text.highCountTrim(100)).toBe('99+');
+      });
+
+      it('returns exact number for count < 100', () => {
+        expect(gl.text.highCountTrim(45)).toBe(45);
+      });
+    });
+
+    describe('gl.text.insertText', () => {
+      let textArea;
+
+      beforeAll(() => {
+        textArea = document.createElement('textarea');
+        document.querySelector('body').appendChild(textArea);
+      });
+
+      afterAll(() => {
+        textArea.parentNode.removeChild(textArea);
+      });
+
+      describe('without selection', () => {
+        it('inserts the tag on an empty line', () => {
+          const initialValue = '';
+
+          textArea.value = initialValue;
+          textArea.selectionStart = 0;
+          textArea.selectionEnd = 0;
+
+          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+          expect(textArea.value).toEqual(`${initialValue}* `);
+        });
+
+        it('inserts the tag on a new line if the current one is not empty', () => {
+          const initialValue = 'some text';
+
+          textArea.value = initialValue;
+          textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+          expect(textArea.value).toEqual(`${initialValue}\n* `);
+        });
+
+        it('inserts the tag on the same line if the current line only contains spaces', () => {
+          const initialValue = '  ';
+
+          textArea.value = initialValue;
+          textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+          expect(textArea.value).toEqual(`${initialValue}* `);
+        });
+
+        it('inserts the tag on the same line if the current line only contains tabs', () => {
+          const initialValue = '\t\t\t';
+
+          textArea.value = initialValue;
+          textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+          expect(textArea.value).toEqual(`${initialValue}* `);
+        });
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
deleted file mode 100644
index 06b69b8ac1750e850a2e24077700c8445f6557ef..0000000000000000000000000000000000000000
--- a/spec/javascripts/lib/utils/text_utility_spec.js.es6
+++ /dev/null
@@ -1,50 +0,0 @@
-require('~/lib/utils/text_utility');
-
-(() => {
-  describe('text_utility', () => {
-    describe('gl.text.getTextWidth', () => {
-      it('returns zero width when no text is passed', () => {
-        expect(gl.text.getTextWidth('')).toBe(0);
-      });
-
-      it('returns zero width when no text is passed and font is passed', () => {
-        expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
-      });
-
-      it('returns width when text is passed', () => {
-        expect(gl.text.getTextWidth('foo') > 0).toBe(true);
-      });
-
-      it('returns bigger width when font is larger', () => {
-        const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
-        const regular = gl.text.getTextWidth('foo', '10px sans-serif');
-        expect(largeFont > regular).toBe(true);
-      });
-    });
-
-    describe('gl.text.pluralize', () => {
-      it('returns pluralized', () => {
-        expect(gl.text.pluralize('test', 2)).toBe('tests');
-      });
-
-      it('returns pluralized when count is 0', () => {
-        expect(gl.text.pluralize('test', 0)).toBe('tests');
-      });
-
-      it('does not return pluralized', () => {
-        expect(gl.text.pluralize('test', 1)).toBe('test');
-      });
-    });
-
-    describe('gl.text.highCountTrim', () => {
-      it('returns 99+ for count >= 100', () => {
-        expect(gl.text.highCountTrim(105)).toBe('99+');
-        expect(gl.text.highCountTrim(100)).toBe('99+');
-      });
-
-      it('returns exact number for count < 100', () => {
-        expect(gl.text.highCountTrim(45)).toBe(45);
-      });
-    });
-  });
-})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index a0b2ebc221bcaf20736875099c8769b548b63906..a1fd2d389687ff831e0d66cbc778546c6820039d 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -7,16 +7,12 @@ require('~/line_highlighter');
   describe('LineHighlighter', function() {
     var clickLine;
     preloadFixtures('static/line_highlighter.html.raw');
-    clickLine = function(number, eventData) {
-      var e;
-      if (eventData == null) {
-        eventData = {};
-      }
+    clickLine = function(number, eventData = {}) {
       if ($.isEmptyObject(eventData)) {
-        return $("#L" + number).mousedown().click();
+        return $("#L" + number).click();
       } else {
-        e = $.Event('mousedown', eventData);
-        return $("#L" + number).trigger(e).click();
+        const e = $.Event('click', eventData);
+        return $("#L" + number).trigger(e);
       }
     };
     beforeEach(function() {
@@ -63,12 +59,6 @@ require('~/line_highlighter');
       });
     });
     describe('#clickHandler', function() {
-      it('discards the mousedown event', function() {
-        var spy;
-        spy = spyOnEvent('a[data-line-number]', 'mousedown');
-        clickLine(13);
-        return expect(spy).toHaveBeenPrevented();
-      });
       it('handles clicking on a child icon element', function() {
         var spy;
         spy = spyOn(this["class"], 'setHash').and.callThrough();
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e504d41d4d4d5c8303db55057a9aeaa87af14e7e
--- /dev/null
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -0,0 +1,72 @@
+/* eslint-disable no-new */
+
+import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import '~/flash';
+
+(() => {
+  describe('Mini Pipeline Graph Dropdown', () => {
+    preloadFixtures('static/mini_dropdown_graph.html.raw');
+
+    beforeEach(() => {
+      loadFixtures('static/mini_dropdown_graph.html.raw');
+    });
+
+    describe('When is initialized', () => {
+      it('should initialize without errors when no options are given', () => {
+        const miniPipelineGraph = new MiniPipelineGraph();
+
+        expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+      });
+
+      it('should set the container as the given prop', () => {
+        const container = '.foo';
+
+        const miniPipelineGraph = new MiniPipelineGraph({ container });
+
+        expect(miniPipelineGraph.container).toEqual(container);
+      });
+    });
+
+    describe('When dropdown is clicked', () => {
+      it('should call getBuildsList', () => {
+        const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+
+        new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+        document.querySelector('.js-builds-dropdown-button').click();
+
+        expect(getBuildsListSpy).toHaveBeenCalled();
+      });
+
+      it('should make a request to the endpoint provided in the html', () => {
+        const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+
+        new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+        document.querySelector('.js-builds-dropdown-button').click();
+        expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+      });
+
+      it('should not close when user uses cmd/ctrl + click', () => {
+        spyOn($, 'ajax').and.callFake(function (params) {
+          params.success({
+            html: `<li>
+              <a class="mini-pipeline-graph-dropdown-item" href="#">
+                <span class="ci-status-icon ci-status-icon-failed"></span>
+                <span class="ci-build-text">build</span>
+              </a>
+              <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+            </li>`,
+          });
+        });
+        new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+        document.querySelector('.js-builds-dropdown-button').click();
+
+        document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+
+        expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
deleted file mode 100644
index 7cdade01e00151b2bba0187c9b109c214ce54bf6..0000000000000000000000000000000000000000
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
+++ /dev/null
@@ -1,51 +0,0 @@
-/* eslint-disable no-new */
-
-require('~/flash');
-require('~/mini_pipeline_graph_dropdown');
-
-(() => {
-  describe('Mini Pipeline Graph Dropdown', () => {
-    preloadFixtures('static/mini_dropdown_graph.html.raw');
-
-    beforeEach(() => {
-      loadFixtures('static/mini_dropdown_graph.html.raw');
-    });
-
-    describe('When is initialized', () => {
-      it('should initialize without errors when no options are given', () => {
-        const miniPipelineGraph = new window.gl.MiniPipelineGraph();
-
-        expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
-      });
-
-      it('should set the container as the given prop', () => {
-        const container = '.foo';
-
-        const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container });
-
-        expect(miniPipelineGraph.container).toEqual(container);
-      });
-    });
-
-    describe('When dropdown is clicked', () => {
-      it('should call getBuildsList', () => {
-        const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
-
-        new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
-
-        document.querySelector('.js-builds-dropdown-button').click();
-
-        expect(getBuildsListSpy).toHaveBeenCalled();
-      });
-
-      it('should make a request to the endpoint provided in the html', () => {
-        const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
-
-        new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
-
-        document.querySelector('.js-builds-dropdown-button').click();
-        expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
-      });
-    });
-  });
-})();
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3c1c5e1b7c1c7a1926567fa3d762d8fd4483eb7
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -0,0 +1,75 @@
+import 'jquery';
+import '~/lib/utils/common_utils';
+import PrometheusGraph from '~/monitoring/prometheus_graph';
+import { prometheusMockData } from './prometheus_mock_data';
+
+describe('PrometheusGraph', () => {
+  const fixtureName = 'static/environments/metrics.html.raw';
+  const prometheusGraphContainer = '.prometheus-graph';
+  const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
+
+  preloadFixtures(fixtureName);
+
+  beforeEach(() => {
+    loadFixtures(fixtureName);
+    this.prometheusGraph = new PrometheusGraph();
+    const self = this;
+    const fakeInit = (metricsResponse) => {
+      self.prometheusGraph.transformData(metricsResponse);
+      self.prometheusGraph.createGraph();
+    };
+    spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit);
+  });
+
+  it('initializes graph properties', () => {
+    // Test for the measurements
+    expect(this.prometheusGraph.margin).toBeDefined();
+    expect(this.prometheusGraph.marginLabelContainer).toBeDefined();
+    expect(this.prometheusGraph.originalWidth).toBeDefined();
+    expect(this.prometheusGraph.originalHeight).toBeDefined();
+    expect(this.prometheusGraph.height).toBeDefined();
+    expect(this.prometheusGraph.width).toBeDefined();
+    expect(this.prometheusGraph.backOffRequestCounter).toBeDefined();
+    // Test for the graph properties (colors, radius, etc.)
+    expect(this.prometheusGraph.graphSpecificProperties).toBeDefined();
+    expect(this.prometheusGraph.commonGraphProperties).toBeDefined();
+  });
+
+  it('transforms the data', () => {
+    this.prometheusGraph.init(prometheusMockData.metrics);
+    expect(this.prometheusGraph.data).toBeDefined();
+    expect(this.prometheusGraph.data.cpu_values.length).toBe(121);
+    expect(this.prometheusGraph.data.memory_values.length).toBe(121);
+  });
+
+  it('creates two graphs', () => {
+    this.prometheusGraph.init(prometheusMockData.metrics);
+    expect($(prometheusGraphContainer).length).toBe(2);
+  });
+
+  describe('Graph contents', () => {
+    beforeEach(() => {
+      this.prometheusGraph.init(prometheusMockData.metrics);
+    });
+
+    it('has axis, an area, a line and a overlay', () => {
+      const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent();
+      expect($graphContainer.find('.x-axis')).toBeDefined();
+      expect($graphContainer.find('.y-axis')).toBeDefined();
+      expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined();
+      expect($graphContainer.find('.metric-line')).toBeDefined();
+      expect($graphContainer.find('.metric-area')).toBeDefined();
+    });
+
+    it('has legends, labels and an extra axis that labels the metrics', () => {
+      const $prometheusGraphContents = $(prometheusGraphContents);
+      const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent();
+      expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined();
+      expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
+      expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
+      expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
+      expect($axisLabelContainer.find('rect').length).toBe(2);
+      expect($axisLabelContainer.find('text').length).toBe(4);
+    });
+  });
+});
diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..1cdc14faaa89e39218c7d640d8f9b315c21fc8ad
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_mock_data.js
@@ -0,0 +1,1014 @@
+/* eslint-disable import/prefer-default-export*/
+export const prometheusMockData = {
+  status: 200,
+  metrics: {
+    success: true,
+    metrics: {
+      memory_values: [
+        {
+          metric: {
+          },
+          values: [
+            [
+              1488462917.256,
+              '10.12890625',
+            ],
+            [
+              1488462977.256,
+              '10.140625',
+            ],
+            [
+              1488463037.256,
+              '10.140625',
+            ],
+            [
+              1488463097.256,
+              '10.14453125',
+            ],
+            [
+              1488463157.256,
+              '10.1484375',
+            ],
+            [
+              1488463217.256,
+              '10.15625',
+            ],
+            [
+              1488463277.256,
+              '10.15625',
+            ],
+            [
+              1488463337.256,
+              '10.15625',
+            ],
+            [
+              1488463397.256,
+              '10.1640625',
+            ],
+            [
+              1488463457.256,
+              '10.171875',
+            ],
+            [
+              1488463517.256,
+              '10.171875',
+            ],
+            [
+              1488463577.256,
+              '10.171875',
+            ],
+            [
+              1488463637.256,
+              '10.18359375',
+            ],
+            [
+              1488463697.256,
+              '10.1953125',
+            ],
+            [
+              1488463757.256,
+              '10.203125',
+            ],
+            [
+              1488463817.256,
+              '10.20703125',
+            ],
+            [
+              1488463877.256,
+              '10.20703125',
+            ],
+            [
+              1488463937.256,
+              '10.20703125',
+            ],
+            [
+              1488463997.256,
+              '10.20703125',
+            ],
+            [
+              1488464057.256,
+              '10.2109375',
+            ],
+            [
+              1488464117.256,
+              '10.2109375',
+            ],
+            [
+              1488464177.256,
+              '10.2109375',
+            ],
+            [
+              1488464237.256,
+              '10.2109375',
+            ],
+            [
+              1488464297.256,
+              '10.21484375',
+            ],
+            [
+              1488464357.256,
+              '10.22265625',
+            ],
+            [
+              1488464417.256,
+              '10.22265625',
+            ],
+            [
+              1488464477.256,
+              '10.2265625',
+            ],
+            [
+              1488464537.256,
+              '10.23046875',
+            ],
+            [
+              1488464597.256,
+              '10.23046875',
+            ],
+            [
+              1488464657.256,
+              '10.234375',
+            ],
+            [
+              1488464717.256,
+              '10.234375',
+            ],
+            [
+              1488464777.256,
+              '10.234375',
+            ],
+            [
+              1488464837.256,
+              '10.234375',
+            ],
+            [
+              1488464897.256,
+              '10.234375',
+            ],
+            [
+              1488464957.256,
+              '10.234375',
+            ],
+            [
+              1488465017.256,
+              '10.23828125',
+            ],
+            [
+              1488465077.256,
+              '10.23828125',
+            ],
+            [
+              1488465137.256,
+              '10.2421875',
+            ],
+            [
+              1488465197.256,
+              '10.2421875',
+            ],
+            [
+              1488465257.256,
+              '10.2421875',
+            ],
+            [
+              1488465317.256,
+              '10.2421875',
+            ],
+            [
+              1488465377.256,
+              '10.2421875',
+            ],
+            [
+              1488465437.256,
+              '10.2421875',
+            ],
+            [
+              1488465497.256,
+              '10.2421875',
+            ],
+            [
+              1488465557.256,
+              '10.2421875',
+            ],
+            [
+              1488465617.256,
+              '10.2421875',
+            ],
+            [
+              1488465677.256,
+              '10.2421875',
+            ],
+            [
+              1488465737.256,
+              '10.2421875',
+            ],
+            [
+              1488465797.256,
+              '10.24609375',
+            ],
+            [
+              1488465857.256,
+              '10.25',
+            ],
+            [
+              1488465917.256,
+              '10.25390625',
+            ],
+            [
+              1488465977.256,
+              '9.98828125',
+            ],
+            [
+              1488466037.256,
+              '9.9921875',
+            ],
+            [
+              1488466097.256,
+              '9.9921875',
+            ],
+            [
+              1488466157.256,
+              '9.99609375',
+            ],
+            [
+              1488466217.256,
+              '10',
+            ],
+            [
+              1488466277.256,
+              '10.00390625',
+            ],
+            [
+              1488466337.256,
+              '10.0078125',
+            ],
+            [
+              1488466397.256,
+              '10.01171875',
+            ],
+            [
+              1488466457.256,
+              '10.0234375',
+            ],
+            [
+              1488466517.256,
+              '10.02734375',
+            ],
+            [
+              1488466577.256,
+              '10.02734375',
+            ],
+            [
+              1488466637.256,
+              '10.03125',
+            ],
+            [
+              1488466697.256,
+              '10.03125',
+            ],
+            [
+              1488466757.256,
+              '10.03125',
+            ],
+            [
+              1488466817.256,
+              '10.03125',
+            ],
+            [
+              1488466877.256,
+              '10.03125',
+            ],
+            [
+              1488466937.256,
+              '10.03125',
+            ],
+            [
+              1488466997.256,
+              '10.03125',
+            ],
+            [
+              1488467057.256,
+              '10.0390625',
+            ],
+            [
+              1488467117.256,
+              '10.0390625',
+            ],
+            [
+              1488467177.256,
+              '10.04296875',
+            ],
+            [
+              1488467237.256,
+              '10.05078125',
+            ],
+            [
+              1488467297.256,
+              '10.05859375',
+            ],
+            [
+              1488467357.256,
+              '10.06640625',
+            ],
+            [
+              1488467417.256,
+              '10.06640625',
+            ],
+            [
+              1488467477.256,
+              '10.0703125',
+            ],
+            [
+              1488467537.256,
+              '10.07421875',
+            ],
+            [
+              1488467597.256,
+              '10.0859375',
+            ],
+            [
+              1488467657.256,
+              '10.0859375',
+            ],
+            [
+              1488467717.256,
+              '10.09765625',
+            ],
+            [
+              1488467777.256,
+              '10.1015625',
+            ],
+            [
+              1488467837.256,
+              '10.10546875',
+            ],
+            [
+              1488467897.256,
+              '10.10546875',
+            ],
+            [
+              1488467957.256,
+              '10.125',
+            ],
+            [
+              1488468017.256,
+              '10.13671875',
+            ],
+            [
+              1488468077.256,
+              '10.1484375',
+            ],
+            [
+              1488468137.256,
+              '10.15625',
+            ],
+            [
+              1488468197.256,
+              '10.16796875',
+            ],
+            [
+              1488468257.256,
+              '10.171875',
+            ],
+            [
+              1488468317.256,
+              '10.171875',
+            ],
+            [
+              1488468377.256,
+              '10.171875',
+            ],
+            [
+              1488468437.256,
+              '10.171875',
+            ],
+            [
+              1488468497.256,
+              '10.171875',
+            ],
+            [
+              1488468557.256,
+              '10.171875',
+            ],
+            [
+              1488468617.256,
+              '10.171875',
+            ],
+            [
+              1488468677.256,
+              '10.17578125',
+            ],
+            [
+              1488468737.256,
+              '10.17578125',
+            ],
+            [
+              1488468797.256,
+              '10.265625',
+            ],
+            [
+              1488468857.256,
+              '10.19921875',
+            ],
+            [
+              1488468917.256,
+              '10.19921875',
+            ],
+            [
+              1488468977.256,
+              '10.19921875',
+            ],
+            [
+              1488469037.256,
+              '10.19921875',
+            ],
+            [
+              1488469097.256,
+              '10.19921875',
+            ],
+            [
+              1488469157.256,
+              '10.203125',
+            ],
+            [
+              1488469217.256,
+              '10.43359375',
+            ],
+            [
+              1488469277.256,
+              '10.20703125',
+            ],
+            [
+              1488469337.256,
+              '10.2109375',
+            ],
+            [
+              1488469397.256,
+              '10.22265625',
+            ],
+            [
+              1488469457.256,
+              '10.21484375',
+            ],
+            [
+              1488469517.256,
+              '10.21484375',
+            ],
+            [
+              1488469577.256,
+              '10.21484375',
+            ],
+            [
+              1488469637.256,
+              '10.22265625',
+            ],
+            [
+              1488469697.256,
+              '10.234375',
+            ],
+            [
+              1488469757.256,
+              '10.234375',
+            ],
+            [
+              1488469817.256,
+              '10.234375',
+            ],
+            [
+              1488469877.256,
+              '10.2421875',
+            ],
+            [
+              1488469937.256,
+              '10.25',
+            ],
+            [
+              1488469997.256,
+              '10.25390625',
+            ],
+            [
+              1488470057.256,
+              '10.26171875',
+            ],
+            [
+              1488470117.256,
+              '10.2734375',
+            ],
+          ],
+        },
+      ],
+      memory_current: [
+        {
+          metric: {
+          },
+          value: [
+            1488470117.737,
+            '10.2734375',
+          ],
+        },
+      ],
+      cpu_values: [
+        {
+          metric: {
+          },
+          values: [
+            [
+              1488462918.15,
+              '0.0002996458625058103',
+            ],
+            [
+              1488462978.15,
+              '0.0002652382333333314',
+            ],
+            [
+              1488463038.15,
+              '0.0003485461333333421',
+            ],
+            [
+              1488463098.15,
+              '0.0003420421999999886',
+            ],
+            [
+              1488463158.15,
+              '0.00023107150000001297',
+            ],
+            [
+              1488463218.15,
+              '0.00030463981666664826',
+            ],
+            [
+              1488463278.15,
+              '0.0002477177833333677',
+            ],
+            [
+              1488463338.15,
+              '0.00026936656666665115',
+            ],
+            [
+              1488463398.15,
+              '0.000406264750000022',
+            ],
+            [
+              1488463458.15,
+              '0.00029592802026561453',
+            ],
+            [
+              1488463518.15,
+              '0.00023426999683316343',
+            ],
+            [
+              1488463578.15,
+              '0.0003057080666666915',
+            ],
+            [
+              1488463638.15,
+              '0.0003408470500000149',
+            ],
+            [
+              1488463698.15,
+              '0.00025497336666665166',
+            ],
+            [
+              1488463758.15,
+              '0.0003009282833333534',
+            ],
+            [
+              1488463818.15,
+              '0.0003119383499999924',
+            ],
+            [
+              1488463878.15,
+              '0.00028719019999998705',
+            ],
+            [
+              1488463938.15,
+              '0.000327864749999988',
+            ],
+            [
+              1488463998.15,
+              '0.0002514917333333422',
+            ],
+            [
+              1488464058.15,
+              '0.0003614651166666742',
+            ],
+            [
+              1488464118.15,
+              '0.0003221668000000122',
+            ],
+            [
+              1488464178.15,
+              '0.00023323083333330884',
+            ],
+            [
+              1488464238.15,
+              '0.00028531499475009274',
+            ],
+            [
+              1488464298.15,
+              '0.0002627695294921391',
+            ],
+            [
+              1488464358.15,
+              '0.00027145463333333453',
+            ],
+            [
+              1488464418.15,
+              '0.00025669488333335266',
+            ],
+            [
+              1488464478.15,
+              '0.00022307761666665965',
+            ],
+            [
+              1488464538.15,
+              '0.0003307265833333517',
+            ],
+            [
+              1488464598.15,
+              '0.0002817050666666709',
+            ],
+            [
+              1488464658.15,
+              '0.00022357458333332285',
+            ],
+            [
+              1488464718.15,
+              '0.00032648590000000275',
+            ],
+            [
+              1488464778.15,
+              '0.00028410750000000816',
+            ],
+            [
+              1488464838.15,
+              '0.0003038076999999954',
+            ],
+            [
+              1488464898.15,
+              '0.00037568226666667335',
+            ],
+            [
+              1488464958.15,
+              '0.00020160354999999202',
+            ],
+            [
+              1488465018.15,
+              '0.0003229403333333399',
+            ],
+            [
+              1488465078.15,
+              '0.00033516069999999236',
+            ],
+            [
+              1488465138.15,
+              '0.0003365978333333371',
+            ],
+            [
+              1488465198.15,
+              '0.00020262178333331585',
+            ],
+            [
+              1488465258.15,
+              '0.00040567498333331876',
+            ],
+            [
+              1488465318.15,
+              '0.00029114155000001436',
+            ],
+            [
+              1488465378.15,
+              '0.0002498841000000122',
+            ],
+            [
+              1488465438.15,
+              '0.00027296763333331715',
+            ],
+            [
+              1488465498.15,
+              '0.0002958794000000135',
+            ],
+            [
+              1488465558.15,
+              '0.0002922354666666867',
+            ],
+            [
+              1488465618.15,
+              '0.00034186624999999653',
+            ],
+            [
+              1488465678.15,
+              '0.0003397984166666627',
+            ],
+            [
+              1488465738.15,
+              '0.0002658284166666469',
+            ],
+            [
+              1488465798.15,
+              '0.00026221139999999346',
+            ],
+            [
+              1488465858.15,
+              '0.00029467960000001034',
+            ],
+            [
+              1488465918.15,
+              '0.0002634141333333358',
+            ],
+            [
+              1488465978.15,
+              '0.0003202958333333209',
+            ],
+            [
+              1488466038.15,
+              '0.00037890760000000394',
+            ],
+            [
+              1488466098.15,
+              '0.00023453356666666518',
+            ],
+            [
+              1488466158.15,
+              '0.0002866827333333433',
+            ],
+            [
+              1488466218.15,
+              '0.0003335935499999998',
+            ],
+            [
+              1488466278.15,
+              '0.00022787131666666125',
+            ],
+            [
+              1488466338.15,
+              '0.00033821938333333064',
+            ],
+            [
+              1488466398.15,
+              '0.00029233375000001043',
+            ],
+            [
+              1488466458.15,
+              '0.00026562758333333514',
+            ],
+            [
+              1488466518.15,
+              '0.0003142600999999819',
+            ],
+            [
+              1488466578.15,
+              '0.00027392178333333444',
+            ],
+            [
+              1488466638.15,
+              '0.00028178598333334173',
+            ],
+            [
+              1488466698.15,
+              '0.0002463400666666911',
+            ],
+            [
+              1488466758.15,
+              '0.00040234373333332125',
+            ],
+            [
+              1488466818.15,
+              '0.00023677453333332822',
+            ],
+            [
+              1488466878.15,
+              '0.00030852703333333523',
+            ],
+            [
+              1488466938.15,
+              '0.0003582272833333455',
+            ],
+            [
+              1488466998.15,
+              '0.0002176380833332973',
+            ],
+            [
+              1488467058.15,
+              '0.00026180203333335447',
+            ],
+            [
+              1488467118.15,
+              '0.00027862966666667436',
+            ],
+            [
+              1488467178.15,
+              '0.0002769731166666567',
+            ],
+            [
+              1488467238.15,
+              '0.0002832899166666477',
+            ],
+            [
+              1488467298.15,
+              '0.0003446533500000311',
+            ],
+            [
+              1488467358.15,
+              '0.0002691345999999761',
+            ],
+            [
+              1488467418.15,
+              '0.000284919933333357',
+            ],
+            [
+              1488467478.15,
+              '0.0002396026166666528',
+            ],
+            [
+              1488467538.15,
+              '0.00035625295000002075',
+            ],
+            [
+              1488467598.15,
+              '0.00036759816666664946',
+            ],
+            [
+              1488467658.15,
+              '0.00030326608333333855',
+            ],
+            [
+              1488467718.15,
+              '0.00023584972418043393',
+            ],
+            [
+              1488467778.15,
+              '0.00025744508892115107',
+            ],
+            [
+              1488467838.15,
+              '0.00036737541666663395',
+            ],
+            [
+              1488467898.15,
+              '0.00034325741666666094',
+            ],
+            [
+              1488467958.15,
+              '0.00026390046666667407',
+            ],
+            [
+              1488468018.15,
+              '0.0003302534500000102',
+            ],
+            [
+              1488468078.15,
+              '0.00035243794999999527',
+            ],
+            [
+              1488468138.15,
+              '0.00020149738333333407',
+            ],
+            [
+              1488468198.15,
+              '0.0003183469666666679',
+            ],
+            [
+              1488468258.15,
+              '0.0003835329166666845',
+            ],
+            [
+              1488468318.15,
+              '0.0002485075333333124',
+            ],
+            [
+              1488468378.15,
+              '0.0003011457166666768',
+            ],
+            [
+              1488468438.15,
+              '0.00032242785497684965',
+            ],
+            [
+              1488468498.15,
+              '0.0002659713747457531',
+            ],
+            [
+              1488468558.15,
+              '0.0003476860333333202',
+            ],
+            [
+              1488468618.15,
+              '0.00028336403333334794',
+            ],
+            [
+              1488468678.15,
+              '0.00017132354999998728',
+            ],
+            [
+              1488468738.15,
+              '0.0003001915833333276',
+            ],
+            [
+              1488468798.15,
+              '0.0003025715666666725',
+            ],
+            [
+              1488468858.15,
+              '0.0003012370166666815',
+            ],
+            [
+              1488468918.15,
+              '0.00030203619999997025',
+            ],
+            [
+              1488468978.15,
+              '0.0002804355000000314',
+            ],
+            [
+              1488469038.15,
+              '0.00033194884999998564',
+            ],
+            [
+              1488469098.15,
+              '0.00025201496666665455',
+            ],
+            [
+              1488469158.15,
+              '0.0002777531500000189',
+            ],
+            [
+              1488469218.15,
+              '0.0003314885833333392',
+            ],
+            [
+              1488469278.15,
+              '0.0002234891422095589',
+            ],
+            [
+              1488469338.15,
+              '0.000349117355867791',
+            ],
+            [
+              1488469398.15,
+              '0.0004036731333333303',
+            ],
+            [
+              1488469458.15,
+              '0.00024553911666667835',
+            ],
+            [
+              1488469518.15,
+              '0.0003056456833333184',
+            ],
+            [
+              1488469578.15,
+              '0.0002618737166666681',
+            ],
+            [
+              1488469638.15,
+              '0.00022972643333331414',
+            ],
+            [
+              1488469698.15,
+              '0.0003713522500000307',
+            ],
+            [
+              1488469758.15,
+              '0.00018322576666666515',
+            ],
+            [
+              1488469818.15,
+              '0.00034534762753952466',
+            ],
+            [
+              1488469878.15,
+              '0.00028200510008501677',
+            ],
+            [
+              1488469938.15,
+              '0.0002773708499999768',
+            ],
+            [
+              1488469998.15,
+              '0.00027547160000001013',
+            ],
+            [
+              1488470058.15,
+              '0.00031713610000000023',
+            ],
+            [
+              1488470118.15,
+              '0.00035276853333332525',
+            ],
+          ],
+        },
+      ],
+      cpu_current: [
+        {
+          metric: {
+          },
+          value: [
+            1488470118.566,
+            '0.00035276853333332525',
+          ],
+        },
+      ],
+      last_update: '2017-03-02T15:55:18.981Z',
+    },
+  },
+};
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index f132537b943aa597cb1f4815969eee5d94c729dc..90a429beeca553bc74331f2c1a53f438013a3dd1 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,6 @@
 /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
 /* global NewBranchForm */
 
-require('jquery-ui/ui/autocomplete');
 require('~/new_branch_form');
 
 (function() {
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d966226909bc9ba1100b9bf8339adc28d24d684f
--- /dev/null
+++ b/spec/javascripts/pager_spec.js
@@ -0,0 +1,90 @@
+/* global fixture */
+
+require('~/pager');
+
+describe('pager', () => {
+  const Pager = window.Pager;
+
+  it('is defined on window', () => {
+    expect(window.Pager).toBeDefined();
+  });
+
+  describe('init', () => {
+    const originalHref = window.location.href;
+
+    beforeEach(() => {
+      setFixtures('<div class="content_list"></div><div class="loading"></div>');
+      spyOn($, 'ajax');
+    });
+
+    afterEach(() => {
+      window.history.replaceState({}, null, originalHref);
+    });
+
+    it('should use data-href attribute from list element', () => {
+      const href = `${gl.TEST_HOST}/some_list.json`;
+      setFixtures(`<div class="content_list" data-href="${href}"></div>`);
+      Pager.init();
+      expect(Pager.url).toBe(href);
+    });
+
+    it('should use current url if data-href attribute not provided', () => {
+      const href = `${gl.TEST_HOST}/some_list`;
+      spyOn(gl.utils, 'removeParams').and.returnValue(href);
+      Pager.init();
+      expect(Pager.url).toBe(href);
+    });
+
+    it('should get initial offset from query parameter', () => {
+      window.history.replaceState({}, null, '?offset=100');
+      Pager.init();
+      expect(Pager.offset).toBe(100);
+    });
+
+    it('keeps extra query parameters from url', () => {
+      window.history.replaceState({}, null, '?filter=test&offset=100');
+      const href = `${gl.TEST_HOST}/some_list?filter=test`;
+      spyOn(gl.utils, 'removeParams').and.returnValue(href);
+      Pager.init();
+      expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+      expect(Pager.url).toEqual(href);
+    });
+  });
+
+  describe('getOld', () => {
+    beforeEach(() => {
+      setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+      Pager.init();
+    });
+
+    it('shows loader while loading next page', () => {
+      spyOn(Pager.loading, 'show');
+      Pager.getOld();
+      expect(Pager.loading.show).toHaveBeenCalled();
+    });
+
+    it('hides loader on success', () => {
+      spyOn($, 'ajax').and.callFake(options => options.success({}));
+      spyOn(Pager.loading, 'hide');
+      Pager.getOld();
+      expect(Pager.loading.hide).toHaveBeenCalled();
+    });
+
+    it('hides loader on error', () => {
+      spyOn($, 'ajax').and.callFake(options => options.error());
+      spyOn(Pager.loading, 'hide');
+      Pager.getOld();
+      expect(Pager.loading.hide).toHaveBeenCalled();
+    });
+
+    it('sends request to url with offset and limit params', () => {
+      spyOn($, 'ajax');
+      Pager.offset = 100;
+      Pager.limit = 20;
+      Pager.getOld();
+      const [{ data, url }] = $.ajax.calls.argsFor(0);
+      expect(data).toBe('limit=20&offset=100');
+      expect(url).toBe('/some_list');
+    });
+  });
+});
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js
similarity index 100%
rename from spec/javascripts/pipelines_spec.js.es6
rename to spec/javascripts/pipelines_spec.js
diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ecaaf1907eaf5d898e243332ab3ece653c5f8354
--- /dev/null
+++ b/spec/javascripts/polyfills/element_spec.js
@@ -0,0 +1,36 @@
+import '~/commons/polyfills/element';
+
+describe('Element polyfills', function () {
+  beforeEach(() => {
+    this.element = document.createElement('ul');
+  });
+
+  describe('matches', () => {
+    it('returns true if element matches the selector', () => {
+      expect(this.element.matches('ul')).toBeTruthy();
+    });
+
+    it("returns false if element doesn't match the selector", () => {
+      expect(this.element.matches('.not-an-element')).toBeFalsy();
+    });
+  });
+
+  describe('closest', () => {
+    beforeEach(() => {
+      this.childElement = document.createElement('li');
+      this.element.appendChild(this.childElement);
+    });
+
+    it('returns the closest parent that matches the selector', () => {
+      expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
+    });
+
+    it('returns itself if it matches the selector', () => {
+      expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
+    });
+
+    it('returns undefined if nothing matches the selector', () => {
+      expect(this.childElement.closest('.no-an-element')).toBeFalsy();
+    });
+  });
+});
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js
similarity index 100%
rename from spec/javascripts/pretty_time_spec.js.es6
rename to spec/javascripts/pretty_time_spec.js
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 69d9587771f302144ecc5d5f35a7f7f6d2bce4f5..3a1d4e2440f777265d211fcf9d5b7efc0b35c010 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -26,7 +26,7 @@ require('~/project');
       var fakeAjaxResponse = function fakeAjaxResponse(req) {
         var d;
         expect(req.url).toBe('/api/v3/projects.json?simple=true');
-        expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 });
+        expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true });
         d = $.Deferred();
         d.resolve(this.projects_data);
         return d.promise();
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 4ac7e911740bc69ccf4e5ad32fb20999b722f2af..285b79401740a20b297bb8a3e8e50b0aca002c58 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,8 +1,8 @@
 /* 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, max-len */
 /* global Sidebar */
 
-require('~/right_sidebar');
-require('~/extensions/jquery.js');
+import '~/commons/bootstrap';
+import '~/right_sidebar';
 
 (function() {
   var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index ffff643e37151d46e1097ef13a45c72c89a27abd..9e19dabd0e3e5efe1d31386f32c5b5a299d0514b 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -31,13 +31,9 @@ require('~/shortcuts_issuable');
           this.shortcut.replyWithSelectedText();
           expect($(this.selector).val()).toBe('');
         });
-        it('triggers `input`', function() {
-          var focused = false;
-          $(this.selector).on('focus', function() {
-            focused = true;
-          });
+        it('triggers `focus`', function() {
           this.shortcut.replyWithSelectedText();
-          expect(focused).toBe(true);
+          expect(document.activeElement).toBe(document.querySelector(this.selector));
         });
       });
       describe('with any selection', function() {
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js
similarity index 100%
rename from spec/javascripts/signin_tabs_memoizer_spec.js.es6
rename to spec/javascripts/signin_tabs_memoizer_spec.js
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js
similarity index 100%
rename from spec/javascripts/smart_interval_spec.js.es6
rename to spec/javascripts/smart_interval_spec.js
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js
similarity index 100%
rename from spec/javascripts/subbable_resource_spec.js.es6
rename to spec/javascripts/subbable_resource_spec.js
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index ca707d872a4a165d9a637c37f96749b01247d15b..5cdb6473edae150c3224bb3467339d698dddee6c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -5,23 +5,12 @@ jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
 jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
 
 // include common libraries
+require('~/commons/index.js');
 window.$ = window.jQuery = require('jquery');
 window._ = require('underscore');
 window.Cookies = require('js-cookie');
 window.Vue = require('vue');
 window.Vue.use(require('vue-resource'));
-require('jquery-ujs');
-require('bootstrap/js/affix');
-require('bootstrap/js/alert');
-require('bootstrap/js/button');
-require('bootstrap/js/collapse');
-require('bootstrap/js/dropdown');
-require('bootstrap/js/modal');
-require('bootstrap/js/scrollspy');
-require('bootstrap/js/tab');
-require('bootstrap/js/transition');
-require('bootstrap/js/tooltip');
-require('bootstrap/js/popover');
 
 // stub expected globals
 window.gl = window.gl || {};
@@ -43,10 +32,11 @@ testsContext.keys().forEach(function (path) {
   }
 });
 
-// workaround: include all source files to find files with 0% coverage
-// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
-describe('Uncovered files', function () {
-  // the following files throw errors because of undefined variables
+// if we're generating coverage reports, make sure to include all files so
+// that we can catch files with 0% coverage
+// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
+if (process.env.BABEL_ENV === 'coverage') {
+  // exempt these files from the coverage report
   const troubleMakers = [
     './blob_edit/blob_edit_bundle.js',
     './cycle_analytics/components/stage_plan_component.js',
@@ -59,21 +49,23 @@ describe('Uncovered files', function () {
     './network/branch_graph.js',
   ];
 
-  const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/);
-  sourceFiles.keys().forEach(function (path) {
-    // ignore if there is a matching spec file
-    if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) {
-      return;
-    }
+  describe('Uncovered files', function () {
+    const sourceFiles = require.context('~', true, /\.js$/);
+    sourceFiles.keys().forEach(function (path) {
+      // ignore if there is a matching spec file
+      if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+        return;
+      }
 
-    it(`includes '${path}'`, function () {
-      try {
-        sourceFiles(path);
-      } catch (err) {
-        if (troubleMakers.indexOf(path) === -1) {
-          expect(err).toBeNull();
+      it(`includes '${path}'`, function () {
+        try {
+          sourceFiles(path);
+        } catch (err) {
+          if (troubleMakers.indexOf(path) === -1) {
+            expect(err).toBeNull();
+          }
         }
-      }
+      });
     });
   });
-});
+}
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..205e72af600fb3fd49d20a0d22ad12e892c56097
--- /dev/null
+++ b/spec/javascripts/user_callout_spec.js
@@ -0,0 +1,57 @@
+const UserCallout = require('~/user_callout');
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+const Cookie = window.Cookies;
+
+describe('UserCallout', function () {
+  const fixtureName = 'static/user_callout.html.raw';
+  preloadFixtures(fixtureName);
+
+  beforeEach(() => {
+    loadFixtures(fixtureName);
+    Cookie.remove(USER_CALLOUT_COOKIE);
+
+    this.userCallout = new UserCallout();
+    this.closeButton = $('.close-user-callout');
+    this.userCalloutBtn = $('.user-callout-btn');
+    this.userCalloutContainer = $('.user-callout');
+  });
+
+  it('does not show when cookie is set not defined', () => {
+    expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined();
+    expect(this.userCalloutContainer.is(':visible')).toBe(true);
+  });
+
+  it('shows when cookie is set to false', () => {
+    Cookie.set(USER_CALLOUT_COOKIE, 'false');
+
+    expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined();
+    expect(this.userCalloutContainer.is(':visible')).toBe(true);
+  });
+
+  it('hides when user clicks on the dismiss-icon', () => {
+    this.closeButton.click();
+    expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+  });
+
+  it('hides when user clicks on the "check it out" button', () => {
+    this.userCalloutBtn.click();
+    expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+  });
+});
+
+describe('UserCallout when cookie is present', function () {
+  const fixtureName = 'static/user_callout.html.raw';
+  preloadFixtures(fixtureName);
+
+  beforeEach(() => {
+    loadFixtures(fixtureName);
+    Cookie.set(USER_CALLOUT_COOKIE, 'true');
+    this.userCallout = new UserCallout();
+    this.userCalloutContainer = $('.user-callout');
+  });
+
+  it('removes the DOM element', () => {
+    expect(this.userCalloutContainer.length).toBe(0);
+  });
+});
diff --git a/spec/javascripts/version_check_image_spec.js b/spec/javascripts/version_check_image_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..464c1fce210df460908c70dde2640f20bb93076f
--- /dev/null
+++ b/spec/javascripts/version_check_image_spec.js
@@ -0,0 +1,33 @@
+const ClassSpecHelper = require('./helpers/class_spec_helper');
+const VersionCheckImage = require('~/version_check_image');
+require('jquery');
+
+describe('VersionCheckImage', function () {
+  describe('.bindErrorEvent', function () {
+    ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
+
+    beforeEach(function () {
+      this.imageElement = $('<div></div>');
+    });
+
+    it('registers an error event', function () {
+      spyOn($.prototype, 'on');
+      spyOn($.prototype, 'off').and.callFake(function () { return this; });
+
+      VersionCheckImage.bindErrorEvent(this.imageElement);
+
+      expect($.prototype.off).toHaveBeenCalledWith('error');
+      expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function));
+    });
+
+    it('hides the imageElement on error', function () {
+      spyOn($.prototype, 'hide');
+
+      VersionCheckImage.bindErrorEvent(this.imageElement);
+
+      this.imageElement.trigger('error');
+
+      expect($.prototype.hide).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js
similarity index 100%
rename from spec/javascripts/visibility_select_spec.js.es6
rename to spec/javascripts/visibility_select_spec.js
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc8e504c4137399f5868708613d21629e3257bd3
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+
+describe('Pipelines Async Button', () => {
+  let component;
+  let spy;
+  let AsyncButtonComponent;
+
+  beforeEach(() => {
+    AsyncButtonComponent = Vue.extend(asyncButtonComp);
+
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+    component = new AsyncButtonComponent({
+      propsData: {
+        endpoint: '/foo',
+        title: 'Foo',
+        icon: 'fa fa-foo',
+        cssClass: 'bar',
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a button', () => {
+    expect(component.$el.tagName).toEqual('BUTTON');
+  });
+
+  it('should render the provided icon', () => {
+    expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
+  });
+
+  it('should render the provided title', () => {
+    expect(component.$el.getAttribute('title')).toContain('Foo');
+    expect(component.$el.getAttribute('aria-label')).toContain('Foo');
+  });
+
+  it('should render the provided cssClass', () => {
+    expect(component.$el.getAttribute('class')).toContain('bar');
+  });
+
+  it('should call the service when it is clicked with the provided endpoint', () => {
+    component.$el.click();
+    expect(spy).toHaveBeenCalledWith('/foo');
+  });
+
+  it('should hide loading if request fails', () => {
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+    component = new AsyncButtonComponent({
+      propsData: {
+        endpoint: '/foo',
+        title: 'Foo',
+        icon: 'fa fa-foo',
+        cssClass: 'bar',
+        dataAttributes: {
+          'data-foo': 'foo',
+        },
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+
+    component.$el.click();
+    expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+  });
+
+  describe('With confirm dialog', () => {
+    it('should call the service when confimation is positive', () => {
+      spyOn(window, 'confirm').and.returnValue(true);
+      spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+      component = new AsyncButtonComponent({
+        propsData: {
+          endpoint: '/foo',
+          title: 'Foo',
+          icon: 'fa fa-foo',
+          cssClass: 'bar',
+          service: {
+            postAction: spy,
+          },
+          confirmActionMessage: 'bar',
+        },
+      }).$mount();
+
+      component.$el.click();
+      expect(spy).toHaveBeenCalledWith('/foo');
+    });
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..96a2a37b5f78aec05e4f34749022159397d61a0b
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+
+describe('Pipeline Url Component', () => {
+  let PipelineUrlComponent;
+
+  beforeEach(() => {
+    PipelineUrlComponent = Vue.extend(pipelineUrlComp);
+  });
+
+  it('should render a table cell', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.tagName).toEqual('TD');
+  });
+
+  it('should render a link the provided path and id', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
+    expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
+  });
+
+  it('should render user information when a user is provided', () => {
+    const mockData = {
+      pipeline: {
+        id: 1,
+        path: 'foo',
+        flags: {},
+        user: {
+          web_url: '/',
+          name: 'foo',
+          avatar_url: '/',
+        },
+      },
+    };
+
+    const component = new PipelineUrlComponent({
+      propsData: mockData,
+    }).$mount();
+
+    const image = component.$el.querySelector('.js-pipeline-url-user img');
+
+    expect(
+      component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
+    ).toEqual(mockData.pipeline.user.web_url);
+    expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+    expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
+  });
+
+  it('should render "API" when no user is provided', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
+  });
+
+  it('should render latest, yaml invalid and stuck flags when provided', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {
+            latest: true,
+            yaml_errors: true,
+            stuck: true,
+          },
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
+    expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
+    expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dba998c7688d356357aa57d627abc9e9ebc7f62e
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+
+describe('Pipelines Actions dropdown', () => {
+  let component;
+  let spy;
+  let actions;
+  let ActionsComponent;
+
+  beforeEach(() => {
+    ActionsComponent = Vue.extend(pipelinesActionsComp);
+
+    actions = [
+      {
+        name: 'stop_review',
+        path: '/root/review-app/builds/1893/play',
+      },
+    ];
+
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+    component = new ActionsComponent({
+      propsData: {
+        actions,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a dropdown with the provided actions', () => {
+    expect(
+      component.$el.querySelectorAll('.dropdown-menu li').length,
+    ).toEqual(actions.length);
+  });
+
+  it('should call the service when an action is clicked', () => {
+    component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+    component.$el.querySelector('.js-pipeline-action-link').click();
+
+    expect(spy).toHaveBeenCalledWith(actions[0].path);
+  });
+
+  it('should hide loading if request fails', () => {
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+    component = new ActionsComponent({
+      propsData: {
+        actions,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+
+    component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+    component.$el.querySelector('.js-pipeline-action-link').click();
+
+    expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7f49649c1c6b52525093416f6c94a713e50a687
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+
+describe('Pipelines Artifacts dropdown', () => {
+  let component;
+  let artifacts;
+
+  beforeEach(() => {
+    const ArtifactsComponent = Vue.extend(artifactsComp);
+
+    artifacts = [
+      {
+        name: 'artifact',
+        path: '/download/path',
+      },
+    ];
+
+    component = new ArtifactsComponent({
+      propsData: {
+        artifacts,
+      },
+    }).$mount();
+  });
+
+  it('should render a dropdown with the provided artifacts', () => {
+    expect(
+      component.$el.querySelectorAll('.dropdown-menu li').length,
+    ).toEqual(artifacts.length);
+  });
+
+  it('should render a link with the provided path', () => {
+    expect(
+      component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+    ).toEqual(artifacts[0].path);
+
+    expect(
+      component.$el.querySelector('.dropdown-menu li a span').textContent,
+    ).toContain(artifacts[0].name);
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c0934404bb5deaec9f91bfcc81c08c81d2b42d9
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
@@ -0,0 +1,72 @@
+import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+
+describe('Pipelines Store', () => {
+  let store;
+
+  beforeEach(() => {
+    store = new PipelineStore();
+  });
+
+  it('should be initialized with an empty state', () => {
+    expect(store.state.pipelines).toEqual([]);
+    expect(store.state.count).toEqual({});
+    expect(store.state.pageInfo).toEqual({});
+  });
+
+  describe('storePipelines', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storePipelines();
+      expect(store.state.pipelines).toEqual([]);
+    });
+
+    it('should store the provided array', () => {
+      const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
+      store.storePipelines(array);
+      expect(store.state.pipelines).toEqual(array);
+    });
+  });
+
+  describe('storeCount', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storeCount();
+      expect(store.state.count).toEqual({});
+    });
+
+    it('should store the provided count', () => {
+      const count = { all: 20, finished: 10 };
+      store.storeCount(count);
+
+      expect(store.state.count).toEqual(count);
+    });
+  });
+
+  describe('storePagination', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storePagination();
+      expect(store.state.pageInfo).toEqual({});
+    });
+
+    it('should store pagination information normalized and parsed', () => {
+      const pagination = {
+        'X-nExt-pAge': '2',
+        'X-page': '1',
+        'X-Per-Page': '1',
+        'X-Prev-Page': '2',
+        'X-TOTAL': '37',
+        'X-Total-Pages': '2',
+      };
+
+      const expectedResult = {
+        perPage: 1,
+        page: 1,
+        total: 37,
+        totalPages: 2,
+        nextPage: 2,
+        previousPage: 2,
+      };
+
+      store.storePagination(pagination);
+      expect(store.state.pageInfo).toEqual(expectedResult);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js
similarity index 87%
rename from spec/javascripts/vue_shared/components/commit_spec.js.es6
rename to spec/javascripts/vue_shared/components/commit_spec.js
index 15ab10b9b6952bdb2874443f495cd7e52da1e468..df547299d75cbd8bc71a55bf5de55dcedff5b928 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,13 +1,17 @@
-require('~/vue_shared/components/commit');
+import Vue from 'vue';
+import commitComp from '~/vue_shared/components/commit';
 
 describe('Commit component', () => {
   let props;
   let component;
+  let CommitComponent;
+
+  beforeEach(() => {
+    CommitComponent = Vue.extend(commitComp);
+  });
 
   it('should render a code-fork icon if it does not represent a tag', () => {
-    setFixtures('<div class="test-commit-container"></div>');
-    component = new window.gl.CommitComponent({
-      el: document.querySelector('.test-commit-container'),
+    component = new CommitComponent({
       propsData: {
         tag: false,
         commitRef: {
@@ -23,15 +27,13 @@ describe('Commit component', () => {
           username: 'jschatz1',
         },
       },
-    });
+    }).$mount();
 
     expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
   });
 
   describe('Given all the props', () => {
     beforeEach(() => {
-      setFixtures('<div class="test-commit-container"></div>');
-
       props = {
         tag: true,
         commitRef: {
@@ -49,10 +51,9 @@ describe('Commit component', () => {
         commitIconSvg: '<svg></svg>',
       };
 
-      component = new window.gl.CommitComponent({
-        el: document.querySelector('.test-commit-container'),
+      component = new CommitComponent({
         propsData: props,
-      });
+      }).$mount();
     });
 
     it('should render a tag icon if it represents a tag', () => {
@@ -105,7 +106,6 @@ describe('Commit component', () => {
 
   describe('When commit title is not provided', () => {
     it('should render default message', () => {
-      setFixtures('<div class="test-commit-container"></div>');
       props = {
         tag: false,
         commitRef: {
@@ -118,10 +118,9 @@ describe('Commit component', () => {
         author: {},
       };
 
-      component = new window.gl.CommitComponent({
-        el: document.querySelector('.test-commit-container'),
+      component = new CommitComponent({
         propsData: props,
-      });
+      }).$mount();
 
       expect(
         component.$el.querySelector('.commit-title span').textContent,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
similarity index 87%
rename from spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
rename to spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 412abfd5e4174622a98d55794df32dcd4a669dd7..699625cdbb7e1ac3bdc1a14efff7988d136890db 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,20 +1,20 @@
-require('~/vue_shared/components/pipelines_table_row');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import tableRowComp from '~/vue_shared/components/pipelines_table_row';
+import pipeline from '../../commit/pipelines/mock_data';
 
 describe('Pipelines Table Row', () => {
   let component;
-  preloadFixtures('static/environments/element.html.raw');
 
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    const PipelinesTableRowComponent = Vue.extend(tableRowComp);
 
-    component = new gl.pipelines.PipelinesTableRowComponent({
+    component = new PipelinesTableRowComponent({
       el: document.querySelector('.test-dom-element'),
       propsData: {
         pipeline,
-        svgs: {},
+        service: {},
       },
-    });
+    }).$mount();
   });
 
   it('should render a table row', () => {
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
similarity index 68%
rename from spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
rename to spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 54d81e2ea7d63e9427c19a3b486f28f3d44778f5..b0b1df5a753d90116df9bec9d0a6440153de9bce 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,24 +1,24 @@
-require('~/vue_shared/components/pipelines_table');
-require('~/lib/utils/datetime_utility');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
+import '~/lib/utils/datetime_utility';
+import pipeline from '../../commit/pipelines/mock_data';
 
 describe('Pipelines Table', () => {
-  preloadFixtures('static/environments/element.html.raw');
+  let PipelinesTableComponent;
 
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    PipelinesTableComponent = Vue.extend(pipelinesTableComp);
   });
 
   describe('table', () => {
     let component;
     beforeEach(() => {
-      component = new gl.pipelines.PipelinesTableComponent({
-        el: document.querySelector('.test-dom-element'),
+      component = new PipelinesTableComponent({
         propsData: {
           pipelines: [],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
     });
 
     it('should render a table', () => {
@@ -37,26 +37,25 @@ describe('Pipelines Table', () => {
 
   describe('without data', () => {
     it('should render an empty table', () => {
-      const component = new gl.pipelines.PipelinesTableComponent({
-        el: document.querySelector('.test-dom-element'),
+      const component = new PipelinesTableComponent({
         propsData: {
           pipelines: [],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
       expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
     });
   });
 
   describe('with data', () => {
     it('should render rows', () => {
-      const component = new gl.pipelines.PipelinesTableComponent({
+      const component = new PipelinesTableComponent({
         el: document.querySelector('.test-dom-element'),
         propsData: {
           pipelines: [pipeline],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
 
       expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
     });
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js
similarity index 62%
rename from spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
rename to spec/javascripts/vue_shared/components/table_pagination_spec.js
index dd495cb43bc855a71dabc1be0622fdaa7d53f811..a5c3870b3acaa10cb2081c5b47067e109d4db864 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -1,24 +1,25 @@
-require('~/lib/utils/common_utils');
-require('~/vue_shared/components/table_pagination');
+import Vue from 'vue';
+import paginationComp from '~/vue_shared/components/table_pagination';
+import '~/lib/utils/common_utils';
 
 describe('Pagination component', () => {
   let component;
+  let PaginationComponent;
 
   const changeChanges = {
     one: '',
-    two: '',
   };
 
-  const change = (one, two) => {
+  const change = (one) => {
     changeChanges.one = one;
-    changeChanges.two = two;
   };
 
-  it('should render and start at page 1', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
+  beforeEach(() => {
+    PaginationComponent = Vue.extend(paginationComp);
+  });
 
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+  it('should render and start at page 1', () => {
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -27,21 +28,17 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     expect(component.$el.classList).toContain('gl-pagination');
 
     component.changePage({ target: { innerText: '1' } });
 
     expect(changeChanges.one).toEqual(1);
-    expect(changeChanges.two).toEqual(null);
   });
 
   it('should go to the previous page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -50,19 +47,15 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Prev' } });
 
     expect(changeChanges.one).toEqual(1);
-    expect(changeChanges.two).toEqual(null);
   });
 
   it('should go to the next page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -71,19 +64,15 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Next' } });
 
     expect(changeChanges.one).toEqual(5);
-    expect(changeChanges.two).toEqual(null);
   });
 
   it('should go to the last page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -92,19 +81,15 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Last >>' } });
 
     expect(changeChanges.one).toEqual(10);
-    expect(changeChanges.two).toEqual(null);
   });
 
   it('should go to the first page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -113,19 +98,15 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: '<< First' } });
 
     expect(changeChanges.one).toEqual(1);
-    expect(changeChanges.two).toEqual(null);
   });
 
   it('should do nothing', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -134,12 +115,11 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: '...' } });
 
     expect(changeChanges.one).toEqual(1);
-    expect(changeChanges.two).toEqual(null);
   });
 });
 
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index c8e62f528df37a27153aab084a2782be1a1d094f..707212e07fd4875b5e221755cf870fdac770f8de 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
 
   it 'replaces supported name emoji' do
     doc = filter('<p>:heart:</p>')
-    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+    expect(doc.css('gl-emoji').first.text).to eq '❤'
   end
 
   it 'replaces supported unicode emoji' do
     doc = filter('<p>❤️</p>')
-    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+    expect(doc.css('gl-emoji').first.text).to eq '❤'
   end
 
   it 'ignores unsupported emoji' do
@@ -30,152 +30,78 @@ describe Banzai::Filter::EmojiFilter, lib: true do
 
   it 'correctly encodes the URL' do
     doc = filter('<p>:+1:</p>')
-    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+    expect(doc.css('gl-emoji').first.text).to eq '👍'
   end
 
   it 'correctly encodes unicode to the URL' do
     doc = filter('<p>👍</p>')
-    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+    expect(doc.css('gl-emoji').first.text).to eq '👍'
   end
 
   it 'matches at the start of a string' do
     doc = filter(':+1:')
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'unicode matches at the start of a string' do
     doc = filter("'👍'")
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'matches at the end of a string' do
     doc = filter('This gets a :-1:')
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'unicode matches at the end of a string' do
     doc = filter('This gets a 👍')
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'matches with adjacent text' do
     doc = filter('+1 (:+1:)')
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'unicode matches with adjacent text' do
     doc = filter('+1 (👍)')
-    expect(doc.css('img').size).to eq 1
+    expect(doc.css('gl-emoji').size).to eq 1
   end
 
   it 'matches multiple emoji in a row' do
     doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
-    expect(doc.css('img').size).to eq 3
+    expect(doc.css('gl-emoji').size).to eq 3
   end
 
   it 'unicode matches multiple emoji in a row' do
     doc = filter("'🙈🙉🙊'")
-    expect(doc.css('img').size).to eq 3
+    expect(doc.css('gl-emoji').size).to eq 3
   end
 
   it 'mixed matches multiple emoji in a row' do
     doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
-    expect(doc.css('img').size).to eq 6
+    expect(doc.css('gl-emoji').size).to eq 6
   end
 
-  it 'has a title attribute' do
+  it 'has a data-name attribute' do
     doc = filter(':-1:')
-    expect(doc.css('img').first.attr('title')).to eq ':-1:'
+    expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
   end
 
-  it 'unicode has a title attribute' do
-    doc = filter("'👎'")
-    expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
-  end
-
-  it 'has an alt attribute' do
+  it 'has a data-unicode-version attribute' do
     doc = filter(':-1:')
-    expect(doc.css('img').first.attr('alt')).to eq ':-1:'
-  end
-
-  it 'unicode has an alt attribute' do
-    doc = filter("'👎'")
-    expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
-  end
-
-  it 'has an align attribute' do
-    doc = filter(':8ball:')
-    expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
-  end
-
-  it 'unicode has an align attribute' do
-    doc = filter("'🎱'")
-    expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
-  end
-
-  it 'has an emoji class' do
-    doc = filter(':cat:')
-    expect(doc.css('img').first.attr('class')).to eq 'emoji'
-  end
-
-  it 'unicode has an emoji class' do
-    doc = filter("'🐱'")
-    expect(doc.css('img').first.attr('class')).to eq 'emoji'
-  end
-
-  it 'has height and width attributes' do
-    doc = filter(':dog:')
-    img = doc.css('img').first
-
-    expect(img.attr('width')).to eq '20'
-    expect(img.attr('height')).to eq '20'
-  end
-
-  it 'unicode has height and width attributes' do
-    doc = filter("'🐶'")
-    img = doc.css('img').first
-
-    expect(img.attr('width')).to eq '20'
-    expect(img.attr('height')).to eq '20'
+    expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
   end
 
   it 'keeps whitespace intact' do
     doc = filter('This deserves a :+1:, big time.')
 
-    expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+    expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
   end
 
   it 'unicode keeps whitespace intact' do
     doc = filter('This deserves a 🎱, big time.')
 
-    expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
-  end
-
-  it 'uses a custom asset_root context' do
-    root = Gitlab.config.gitlab.url + 'gitlab/root'
-
-    doc = filter(':smile:', asset_root: root)
-    expect(doc.css('img').first.attr('src')).to start_with(root)
-  end
-
-  it 'uses a custom asset_host context' do
-    ActionController::Base.asset_host = 'https://cdn.example.com'
-
-    doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
-    expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
-  end
-
-  it 'uses a custom asset_root context' do
-    root = Gitlab.config.gitlab.url + 'gitlab/root'
-
-    doc = filter("'🎱'", asset_root: root)
-    expect(doc.css('img').first.attr('src')).to start_with(root)
-  end
-
-  it 'uses a custom asset_host context' do
-    ActionController::Base.asset_host = 'https://cdn.example.com'
-
-    doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
-    expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+    expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
   end
 end
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index a2a1ed58d1bda4bfaf54e2ac5da96c80c422b528..294558b3db233e4eee2dc83a08ebcef20e59bef9 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
   end
 
   it 'does not wrap a duplicate link' do
-    exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)
-    expect(filter(act).to_html).to eq exp
+    doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
+    expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
   end
 
   it 'works with external images' do
@@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
     expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
   end
 
-  it 'wraps the image with a link and a div' do
-    doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
-    expect(doc.to_html).to include('<div class="image-container">')
+  it 'works with inline images' do
+    doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
+    expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
   end
 end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index b38e3b17e64b062aa6e60ceb61ee90a41d1f3afe..b4cd5f63a15242cf0693c806ef09a7bd5e9cc00e 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -86,6 +86,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
       expect(filter(act).to_html).to eq exp
     end
 
+    it 'allows `summary` elements' do
+      exp = act = '<summary>summary line</summary>'
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'allows `details` elements' do
+      exp = act = '<details>long text goes here</details>'
+      expect(filter(act).to_html).to eq exp
+    end
+
     it 'removes `rel` attribute from `a` elements' do
       act = %q{<a href="#" rel="nofollow">Link</a>}
       exp = %q{<a href="#">Link</a>}
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 69e3c52b35af00df2089e326930896447a325ea2..63fb1bb25c4108b89569a8f2f96c8be72515ccca 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
   context "when no language is specified" do
     it "highlights as plaintext" do
       result = filter('<pre><code>def fun end</code></pre>')
-      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
     end
   end
 
   context "when a valid language is specified" do
     it "highlights as that language" do
       result = filter('<pre><code class="ruby">def fun end</code></pre>')
-      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
     end
   end
 
   context "when an invalid language is specified" do
     it "highlights as plaintext" do
       result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
-      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
     end
   end
 
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index d5d128c19075d94f3f39702bf5e309c33c936127..9873774909e3021f111e4be28a51be89805c4e63 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -123,6 +123,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
 
       expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
     end
+
+    it 'has the full group name as a title' do
+      doc = reference_filter("Hey #{reference}")
+
+      expect(doc.css('a').first.attr('title')).to eq group.full_name
+    end
   end
 
   it 'links with adjacent text' do
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
index 015a7f80e03441d932696ff7f530c5e37f698d96..9008cb3e87025af3ab0e126cc5845c7b1af03a06 100644
--- a/spec/lib/bitbucket/collection_spec.rb
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -19,6 +19,6 @@ 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"])
+    expect(collection.to_a).to match(%w(result_1_page_1 result_2_page_1 result_1_page_2 result_2_page_2))
   end
 end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index adcd978e1b3b6dffb74c3b2d80684f88bbdad34c..405265cc6691077625ab89556742e310a416827c 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -29,7 +29,7 @@ describe Bitbucket::Representation::Repo do
   end
 
   describe '#owner_and_slug' do
-    it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) }
+    it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(%w(ben test)) }
   end
 
   describe '#owner' do
@@ -42,7 +42,7 @@ describe Bitbucket::Representation::Repo do
 
   describe '#clone_url' do
     it 'builds url' do
-      data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
+      data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
       expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git')
     end
   end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 68ad429608d9831193ac85dea68115380826e503..53abc056602eb3a47378d588ea867e2432435da1 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -15,9 +15,9 @@ module Ci
     end
 
     describe '#build_attributes' do
-      describe 'coverage entry' do
-        subject { described_class.new(config, path).build_attributes(:rspec) }
+      subject { described_class.new(config, path).build_attributes(:rspec) }
 
+      describe 'coverage entry' do
         describe 'code coverage regexp' do
           let(:config) do
             YAML.dump(rspec: { script: 'rspec',
@@ -30,6 +30,56 @@ module Ci
           end
         end
       end
+
+      describe 'allow failure entry' do
+        context 'when job is a manual action' do
+          context 'when allow_failure is defined' do
+            let(:config) do
+              YAML.dump(rspec: { script: 'rspec',
+                                 when: 'manual',
+                                 allow_failure: false })
+            end
+
+            it 'is not allowed to fail' do
+              expect(subject[:allow_failure]).to be false
+            end
+          end
+
+          context 'when allow_failure is not defined' do
+            let(:config) do
+              YAML.dump(rspec: { script: 'rspec',
+                                 when: 'manual' })
+            end
+
+            it 'is allowed to fail' do
+              expect(subject[:allow_failure]).to be true
+            end
+          end
+        end
+
+        context 'when job is not a manual action' do
+          context 'when allow_failure is defined' do
+            let(:config) do
+              YAML.dump(rspec: { script: 'rspec',
+                                 allow_failure: false })
+            end
+
+            it 'is not allowed to fail' do
+              expect(subject[:allow_failure]).to be false
+            end
+          end
+
+          context 'when allow_failure is not defined' do
+            let(:config) do
+              YAML.dump(rspec: { script: 'rspec' })
+            end
+
+            it 'is not allowed to fail' do
+              expect(subject[:allow_failure]).to be false
+            end
+          end
+        end
+      end
     end
 
     describe "#builds_for_ref" do
@@ -96,7 +146,7 @@ module Ci
         it "returns builds if only has a list of branches including specified" do
           config = YAML.dump({
                                before_script: ["pwd"],
-                               rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+                               rspec: { script: "rspec", type: type, only: %w(master deploy) }
                              })
 
           config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -173,8 +223,8 @@ module Ci
         it "returns build only for specified type" do
           config = YAML.dump({
                                before_script: ["pwd"],
-                               rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
-                               staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+                               rspec: { script: "rspec", type: "test", only: %w(master deploy) },
+                               staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
                                production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
                              })
 
@@ -252,7 +302,7 @@ module Ci
         it "does not return builds if except has a list of branches including specified" do
           config = YAML.dump({
                                before_script: ["pwd"],
-                               rspec: { script: "rspec", type: type, except: ["master", "deploy"] }
+                               rspec: { script: "rspec", type: type, except: %w(master deploy) }
                              })
 
           config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -580,7 +630,7 @@ module Ci
         context 'when syntax is incorrect' do
           context 'when variables defined but invalid' do
             let(:variables) do
-              [ 'VAR1', 'value1', 'VAR2', 'value2' ]
+              %w(VAR1 value1 VAR2 value2)
             end
 
             it 'raises error' do
@@ -909,7 +959,7 @@ module Ci
       end
 
       context 'dependencies to builds' do
-        let(:dependencies) { ['build1', 'build2'] }
+        let(:dependencies) { %w(build1 build2) }
 
         it { expect { subject }.not_to raise_error }
       end
@@ -1215,7 +1265,7 @@ EOT
       end
 
       it "returns errors if job stage is not a defined stage" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
         expect do
           GitlabCiYamlProcessor.new(config, path)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
@@ -1257,42 +1307,42 @@ EOT
       end
 
       it "returns errors if job artifacts:name is not an a string" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
       end
 
       it "returns errors if job artifacts:when is not an a predefined value" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
       end
 
       it "returns errors if job artifacts:expire_in is not an a string" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
       end
 
       it "returns errors if job artifacts:expire_in is not an a valid duration" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
       end
 
       it "returns errors if job artifacts:untracked is not an array of strings" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
       end
 
       it "returns errors if job artifacts:paths is not an array of strings" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
@@ -1320,28 +1370,28 @@ EOT
       end
 
       it "returns errors if job cache:key is not an a string" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
       end
 
       it "returns errors if job cache:untracked is not an array of strings" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
       end
 
       it "returns errors if job cache:paths is not an array of strings" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
       end
 
       it "returns errors if job dependencies is not an array of strings" do
-        config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+        config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
         expect do
           GitlabCiYamlProcessor.new(config)
         end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index a5251e9a8c25cded8c7dc5b052cc79627b7c56d3..4f25ad88960500a5cb119902c9637c076c50a713 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -6,7 +6,7 @@ describe ProjectUrlConstrainer, lib: true do
 
   describe '#matches?' do
     context 'valid request' do
-      let(:request) { build_request(namespace.path, project.path) }
+      let(:request) { build_request(namespace.full_path, project.path) }
 
       it { expect(subject.matches?(request)).to be_truthy }
     end
@@ -19,7 +19,7 @@ describe ProjectUrlConstrainer, lib: true do
       end
 
       context "project id ending with .git" do
-        let(:request) { build_request(namespace.path, project.path + '.git') }
+        let(:request) { build_request(namespace.full_path, project.path + '.git') }
 
         it { expect(subject.matches?(request)).to be_falsey }
       end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90bc7dad3799e42e3d8149a13a42fd34b1526ad9..906289179438a19c32025cfcd06bc04056c8e1bc 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -7,58 +7,49 @@ describe ExpandVariables do
     tests = [
       { value: 'key',
         result: 'key',
-        variables: []
-      },
+        variables: [] },
       { value: 'key$variable',
         result: 'key',
-        variables: []
-      },
+        variables: [] },
       { value: 'key$variable',
         result: 'keyvalue',
         variables: [
           { key: 'variable', value: 'value' }
-        ]
-      },
+        ] },
       { value: 'key${variable}',
         result: 'keyvalue',
         variables: [
           { key: 'variable', value: 'value' }
-        ]
-      },
+        ] },
       { value: 'key$variable$variable2',
         result: 'keyvalueresult',
         variables: [
           { key: 'variable', value: 'value' },
           { key: 'variable2', value: 'result' },
-        ]
-      },
+        ] },
       { value: 'key${variable}${variable2}',
         result: 'keyvalueresult',
         variables: [
           { key: 'variable', value: 'value' },
           { key: 'variable2', value: 'result' }
-        ]
-      },
+        ] },
       { value: 'key$variable2$variable',
         result: 'keyresultvalue',
         variables: [
           { key: 'variable', value: 'value' },
           { key: 'variable2', value: 'result' },
-        ]
-      },
+        ] },
       { value: 'key${variable2}${variable}',
         result: 'keyresultvalue',
         variables: [
           { key: 'variable', value: 'value' },
           { key: 'variable2', value: 'result' }
-        ]
-      },
-      { value: 'review/$CI_BUILD_REF_NAME',
+        ] },
+      { value: 'review/$CI_COMMIT_REF_NAME',
         result: 'review/feature/add-review-apps',
         variables: [
-          { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
-        ]
-      },
+          { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
+        ] },
     ]
 
     tests.each do |test|
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 29c07655ae8e12a515e0454163802a221613bf99..33ab005667a01755e2d07495127a666ca935839d 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -177,12 +177,12 @@ describe ExtractsPath, lib: true do
 
       it "extracts a valid commit SHA" do
         expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
-          ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG']
+          %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
         )
       end
 
       it "falls back to a primitive split for an invalid ref" do
-        expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG'])
+        expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
       end
     end
   end
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb
index dc57e94f193b0e5992c4f3ee0e193c5842d95be7..cc8daa535d6ba12b2b8b7193da6e9f779dd299a9 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/git_ref_validator_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do
   it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy }
   it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy }
   it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy }
+  it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy }
   it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey }
   it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey }
   it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey }
@@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do
   it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey }
   it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey }
   it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey }
+  it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey }
+  it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey }
+  it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey }
+  it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey }
 end
diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94dcddcc30c3927c21d5a4aa3267fad899187650
--- /dev/null
+++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do
+  include_context 'unique ips sign in limit'
+  let(:user) { create(:user) }
+
+  describe '#count_unique_ips' do
+    context 'non unique IPs' do
+      it 'properly counts them' do
+        expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1)
+        expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1)
+      end
+    end
+
+    context 'unique IPs' do
+      it 'properly counts them' do
+        expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
+        expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
+      end
+    end
+
+    it 'resets count after specified time window' do
+      Timecop.freeze do
+        expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
+        expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
+
+        Timecop.travel(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
+          expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1)
+          expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2)
+        end
+      end
+    end
+  end
+
+  describe '#limit_user!' do
+    include_examples 'user login operation with unique ip limit' do
+      def operation
+        described_class.limit_user! { user }
+      end
+    end
+
+    context 'allow 2 unique ips' do
+      before { current_application_settings.update!(unique_ips_limit_per_user: 2) }
+
+      it 'blocks user trying to login from third ip' do
+        change_ip('ip1')
+        expect(described_class.limit_user! { user }).to eq(user)
+
+        change_ip('ip2')
+        expect(described_class.limit_user! { user }).to eq(user)
+
+        change_ip('ip3')
+        expect { described_class.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b234de4c77267fa3ede434a071366b65332c176e..03c4879ed6f92f44c0777084edc0e24e8b9ef242 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -3,6 +3,24 @@ require 'spec_helper'
 describe Gitlab::Auth, lib: true do
   let(:gl_auth) { described_class }
 
+  describe 'constants' do
+    it 'API_SCOPES contains all scopes for API access' do
+      expect(subject::API_SCOPES).to eq [:api, :read_user]
+    end
+
+    it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
+      expect(subject::OPENID_SCOPES).to eq [:openid]
+    end
+
+    it 'DEFAULT_SCOPES contains all default scopes' do
+      expect(subject::DEFAULT_SCOPES).to eq [:api]
+    end
+
+    it 'OPTIONAL_SCOPES contains all non-default scopes' do
+      expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid]
+    end
+  end
+
   describe 'find_for_git_client' do
     context 'build token' do
       subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
@@ -58,6 +76,14 @@ describe Gitlab::Auth, lib: true do
       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
 
+    include_examples 'user login operation with unique ip limit' do
+      let(:user) { create(:user, password: 'password') }
+
+      def operation
+        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
+    end
+
     context 'while using LFS authenticate' do
       it 'recognizes user lfs tokens' do
         user = create(:user)
@@ -110,25 +136,37 @@ describe Gitlab::Auth, lib: true do
     end
 
     context 'while using personal access tokens as passwords' do
-      let(:user) { create(:user) }
-      let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
-
       it 'succeeds for personal access tokens with the `api` scope' do
-        expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
-        expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+        personal_access_token = create(:personal_access_token, scopes: ['api'])
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+        expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
+      end
+
+      it 'succeeds if it is an impersonation token' do
+        impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+        expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities))
       end
 
       it 'fails for personal access tokens with other scopes' do
-        personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+        personal_access_token = create(:personal_access_token, 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))
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+        expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
       end
 
-      it 'does not try password auth before personal access tokens' do
-        expect(gl_auth).not_to receive(:find_with_user_password)
+      it 'fails for impersonation token with other scopes' do
+        impersonation_token = create(:personal_access_token, scopes: ['read_user'])
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+        expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+      end
 
-        gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+      it 'fails if password is nil' do
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+        expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
       end
     end
 
@@ -196,6 +234,24 @@ describe Gitlab::Auth, lib: true do
       expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
     end
 
+    include_examples 'user login operation with unique ip limit' do
+      def operation
+        expect(gl_auth.find_with_user_password(username, password)).to eq(user)
+      end
+    end
+
+    it "does not find user in blocked state" do
+      user.block
+
+      expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+    end
+
+    it "does not find user in ldap_blocked state" do
+      user.ldap_block
+
+      expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+    end
+
     context "with ldap enabled" do
       before do
         allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
diff --git a/spec/lib/gitlab/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
deleted file mode 100644
index 00a110e31f85272c51e76a9c6071fecf1265bd55..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/award_emoji_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AwardEmoji do
-  describe '.urls' do
-    after do
-      Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
-    end
-
-    subject { Gitlab::AwardEmoji.urls }
-
-    it { is_expected.to be_an_instance_of(Array) }
-    it { is_expected.not_to be_empty }
-
-    context 'every Hash in the Array' do
-      it 'has the correct keys and values' do
-        subject.each do |hash|
-          expect(hash[:name]).to be_an_instance_of(String)
-          expect(hash[:path]).to be_an_instance_of(String)
-        end
-      end
-    end
-
-    context 'handles relative root' do
-      it 'includes the full path' do
-        allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
-
-        subject.each do |hash|
-          expect(hash[:name]).to be_an_instance_of(String)
-          expect(hash[:path]).to start_with('/gitlab')
-        end
-      end
-    end
-  end
-
-  describe '.emoji_by_category' do
-    it "only contains known categories" do
-      undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
-      expect(undefined_categories).to be_empty
-    end
-  end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 0a2fe5af2c372becaea35c2e1e5ba60830e54da2..a7ee7f53a6b23d2d47267855bc0b4a6b75e105dc 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -87,10 +87,10 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
                   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: {})
+        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(
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
index 5b678d31fce40d90918ea703b140771a266e9e60..3916fc704a4d91ab57eae93db1b48172bed6bba6 100644
--- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -26,6 +26,21 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do
     end
   end
 
+  context 'with labels' do
+    let(:label) { create(:label, project: project, title: 'mep') }
+    let(:label1) { create(:label, project: project, title: 'mop') }
+
+    before do
+      issue.labels << [label, label1]
+    end
+
+    it 'shows the labels' do
+      labels = attachment[:fields].find { |f| f[:title] == 'Labels' }
+
+      expect(labels[:value]).to eq("mep, mop")
+    end
+  end
+
   context 'confidential issue' do
     let(:issue) { create(:issue, project: project) }
 
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index cadfbadca10c18917122e4133aee5f1dfa8651ea..e22f88b7a32a62c62536c2c535a88c45156db2eb 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -12,8 +12,16 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
         ref: 'refs/heads/master'
       }
     end
-
-    subject { described_class.new(changes, project: project, user_access: user_access).exec }
+    let(:protocol) { 'ssh' }
+
+    subject do
+      described_class.new(
+        changes,
+        project: project,
+        user_access: user_access,
+        protocol: protocol
+      ).exec
+    end
 
     before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
 
diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..382385dfd6bd3adae478693c1a7f7fef198317d8
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/image_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Image do
+  let(:job) { create(:ci_build, :no_options) }
+
+  describe '#from_image' do
+    subject { described_class.from_image(job) }
+
+    context 'when image is defined in job' do
+      let(:image_name) { 'ruby:2.1' }
+      let(:job) { create(:ci_build, options: { image: image_name } ) }
+
+      it 'fabricates an object of the proper class' do
+        is_expected.to be_kind_of(described_class)
+      end
+
+      it 'populates fabricated object with the proper name attribute' do
+        expect(subject.name).to eq(image_name)
+      end
+
+      context 'when image name is empty' do
+        let(:image_name) { '' }
+
+        it 'does not fabricate an object' do
+          is_expected.to be_nil
+        end
+      end
+    end
+
+    context 'when image is not defined in job' do
+      it 'does not fabricate an object' do
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#from_services' do
+    subject { described_class.from_services(job) }
+
+    context 'when services are defined in job' do
+      let(:service_image_name) { 'postgres' }
+      let(:job) { create(:ci_build, options: { services: [service_image_name] }) }
+
+      it 'fabricates an non-empty array of objects' do
+        is_expected.to be_kind_of(Array)
+        is_expected.not_to be_empty
+        expect(subject.first.name).to eq(service_image_name)
+      end
+
+      context 'when service image name is empty' do
+        let(:service_image_name) { '' }
+
+        it 'fabricates an empty array' do
+          is_expected.to be_kind_of(Array)
+          is_expected.to be_empty
+        end
+      end
+    end
+
+    context 'when services are not defined in job' do
+      it 'fabricates an empty array' do
+        is_expected.to be_kind_of(Array)
+        is_expected.to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a314a744ca8e5a89e0b311d3fda6ba77f40447f
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/step_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Step do
+  let(:job) { create(:ci_build, :no_options, commands: "ls -la\ndate") }
+
+  describe '#from_commands' do
+    subject { described_class.from_commands(job) }
+
+    it 'fabricates an object' do
+      expect(subject.name).to eq(:script)
+      expect(subject.script).to eq(['ls -la', 'date'])
+      expect(subject.timeout).to eq(job.timeout)
+      expect(subject.when).to eq('on_success')
+      expect(subject.allow_failure).to be_falsey
+    end
+  end
+
+  describe '#from_after_script' do
+    subject { described_class.from_after_script(job) }
+
+    context 'when after_script is empty' do
+      it 'doesn not fabricate an object' do
+        is_expected.to be_nil
+      end
+    end
+
+    context 'when after_script is not empty' do
+      let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) }
+
+      it 'fabricates an object' do
+        expect(subject.name).to eq(:after_script)
+        expect(subject.script).to eq(['ls -la', 'date'])
+        expect(subject.timeout).to eq(job.timeout)
+        expect(subject.when).to eq('always')
+        expect(subject.allow_failure).to be_truthy
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 70a327c5183c3b16780899bb98d1eddeb528af61..2ed120f356a075d95321f6bd6018e1fa190967b8 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -24,6 +24,20 @@ describe Gitlab::Ci::Config::Entry::Cache do
           expect(entry).to be_valid
         end
       end
+
+      context 'when key is missing' do
+        let(:config) do
+          { untracked: true,
+            paths: ['some/path/'] }
+        end
+
+        describe '#value' do
+          it 'sets key with the default' do
+            expect(entry.value[:key])
+              .to eq(Gitlab::Ci::Config::Entry::Key.default)
+          end
+        end
+      end
     end
 
     context 'when entry value is not correct' do
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index b8b0825a1c7f5323d60e48050633313b8ca78d32..afa4a089418ebaf3aa12aa1bcf1c329d8f94e008 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Entry::Commands do
   let(:entry) { described_class.new(config) }
 
   context 'when entry config value is an array' do
-    let(:config) { ['ls', 'pwd'] }
+    let(:config) { %w(ls pwd) }
 
     describe '#value' do
       it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 2adbed2154fcd79c390d30bb5fd7f8debf753381..c330e609337756e11859e5a537b2d3135a887da1 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -151,8 +151,8 @@ describe Gitlab::Ci::Config::Entry::Environment do
 
   context 'when variables are used for environment' do
     let(:config) do
-      { name: 'review/$CI_BUILD_REF_NAME',
-        url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+      { name: 'review/$CI_COMMIT_REF_NAME',
+        url: 'https://$CI_COMMIT_REF_NAME.review.gitlab.com' }
     end
 
     describe '#valid?' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
index 00dad5d959131925df823fa8b56312b838c2e3f1..8dd48e4efae5ac8d079c020ba6c32ab6ad7eee4d 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -8,20 +8,20 @@ describe Gitlab::Ci::Config::Entry::Factory do
     context 'when setting a concrete value' do
       it 'creates entry with valid value' do
         entry = factory
-          .value(['ls', 'pwd'])
+          .value(%w(ls pwd))
           .create!
 
-        expect(entry.value).to eq ['ls', 'pwd']
+        expect(entry.value).to eq %w(ls pwd)
       end
 
       context 'when setting description' do
         it 'creates entry with description' do
           entry = factory
-            .value(['ls', 'pwd'])
+            .value(%w(ls pwd))
             .with(description: 'test description')
             .create!
 
-          expect(entry.value).to eq ['ls', 'pwd']
+          expect(entry.value).to eq %w(ls pwd)
           expect(entry.description).to eq 'test description'
         end
       end
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
       context 'when setting key' do
         it 'creates entry with custom key' do
           entry = factory
-            .value(['ls', 'pwd'])
+            .value(%w(ls pwd))
             .with(key: 'test key')
             .create!
 
@@ -60,13 +60,13 @@ describe Gitlab::Ci::Config::Entry::Factory do
     end
 
     context 'when creating entry with nil value' do
-      it 'creates an undefined entry' do
+      it 'creates an unspecified entry' do
         entry = factory
           .value(nil)
           .create!
 
         expect(entry)
-          .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+          .not_to be_specified
       end
     end
 
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 432a99dce33f12ed1b574b2b92583d780d579454..684d01e9056ebaac2f8cb625f844fd8617681044 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -21,12 +21,12 @@ describe Gitlab::Ci::Config::Entry::Global do
   context 'when configuration is valid' do
     context 'when some entries defined' do
       let(:hash) do
-        { before_script: ['ls', 'pwd'],
+        { before_script: %w(ls pwd),
           image: 'ruby:2.2',
           services: ['postgres:9.1', 'mysql:5.5'],
           variables: { VAR: 'value' },
           after_script: ['make clean'],
-          stages: ['build', 'pages'],
+          stages: %w(build pages),
           cache: { key: 'k', untracked: true, paths: ['public/'] },
           rspec: { script: %w[rspec ls] },
           spinach: { before_script: [], variables: {}, script: 'spinach' } }
@@ -89,7 +89,7 @@ describe Gitlab::Ci::Config::Entry::Global do
 
         describe '#before_script_value' do
           it 'returns correct script' do
-            expect(global.before_script_value).to eq ['ls', 'pwd']
+            expect(global.before_script_value).to eq %w(ls pwd)
           end
         end
 
@@ -126,7 +126,7 @@ describe Gitlab::Ci::Config::Entry::Global do
 
           context 'when deprecated types key defined' do
             let(:hash) do
-              { types: ['test', 'deploy'],
+              { types: %w(test deploy),
                 rspec: { script: 'rspec' } }
             end
 
@@ -148,13 +148,14 @@ describe Gitlab::Ci::Config::Entry::Global do
             expect(global.jobs_value).to eq(
               rspec: { name: :rspec,
                        script: %w[rspec ls],
-                       before_script: ['ls', 'pwd'],
+                       before_script: %w(ls pwd),
                        commands: "ls\npwd\nrspec\nls",
                        image: 'ruby:2.2',
                        services: ['postgres:9.1', 'mysql:5.5'],
                        stage: 'test',
                        cache: { key: 'k', untracked: true, paths: ['public/'] },
                        variables: { VAR: 'value' },
+                       ignore: false,
                        after_script: ['make clean'] },
               spinach: { name: :spinach,
                          before_script: [],
@@ -165,6 +166,7 @@ describe Gitlab::Ci::Config::Entry::Global do
                          stage: 'test',
                          cache: { key: 'k', untracked: true, paths: ['public/'] },
                          variables: {},
+                         ignore: false,
                          after_script: ['make clean'] },
             )
           end
@@ -186,7 +188,7 @@ describe Gitlab::Ci::Config::Entry::Global do
 
         it 'contains unspecified nodes' do
           expect(global.descendants.first)
-            .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+            .not_to be_specified
         end
       end
 
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index d20f4ec207d68bc8b49004455bc73d235586901d..9249bb9c17299a9eecada649aa0bed937aad693e 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -144,6 +144,7 @@ describe Gitlab::Ci::Config::Entry::Job do
                    script: %w[rspec],
                    commands: "ls\npwd\nrspec",
                    stage: 'test',
+                   ignore: false,
                    after_script: %w[cleanup])
         end
       end
@@ -159,4 +160,82 @@ describe Gitlab::Ci::Config::Entry::Job do
       end
     end
   end
+
+  describe '#manual_action?' do
+    context 'when job is a manual action' do
+      let(:config) { { script: 'deploy', when: 'manual' } }
+
+      it 'is a manual action' do
+        expect(entry).to be_manual_action
+      end
+    end
+
+    context 'when job is not a manual action' do
+      let(:config) { { script: 'deploy' } }
+
+      it 'is not a manual action' do
+        expect(entry).not_to be_manual_action
+      end
+    end
+  end
+
+  describe '#ignored?' do
+    context 'when job is a manual action' do
+      context 'when it is not specified if job is allowed to fail' do
+        let(:config) do
+          { script: 'deploy', when: 'manual' }
+        end
+
+        it 'is an ignored job' do
+          expect(entry).to be_ignored
+        end
+      end
+
+      context 'when job is allowed to fail' do
+        let(:config) do
+          { script: 'deploy', when: 'manual', allow_failure: true }
+        end
+
+        it 'is an ignored job' do
+          expect(entry).to be_ignored
+        end
+      end
+
+      context 'when job is not allowed to fail' do
+        let(:config) do
+          { script: 'deploy', when: 'manual', allow_failure: false }
+        end
+
+        it 'is not an ignored job' do
+          expect(entry).not_to be_ignored
+        end
+      end
+    end
+
+    context 'when job is not a manual action' do
+      context 'when it is not specified if job is allowed to fail' do
+        let(:config) { { script: 'deploy' } }
+
+        it 'is not an ignored job' do
+          expect(entry).not_to be_ignored
+        end
+      end
+
+      context 'when job is allowed to fail' do
+        let(:config) { { script: 'deploy', allow_failure: true } }
+
+        it 'is an ignored job' do
+          expect(entry).to be_ignored
+        end
+      end
+
+      context 'when job is not allowed to fail' do
+        let(:config) { { script: 'deploy', allow_failure: false } }
+
+        it 'is not an ignored job' do
+          expect(entry).not_to be_ignored
+        end
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index aaebf7839624aa3d798c46e852630812935e5e66..7d104372ac65d3e94e7785e225e6f9246322d34c 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -62,10 +62,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do
           rspec: { name: :rspec,
                    script: %w[rspec],
                    commands: 'rspec',
+                   ignore: false,
                    stage: 'test' },
           spinach: { name: :spinach,
                      script: %w[spinach],
                      commands: 'spinach',
+                     ignore: false,
                      stage: 'test' })
       end
     end
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index a55e5b4b8ac97ad3ea0d1c793aff37fe928a15ee..5d4de60bc8aa36296c844ae449a4e9bc9a78eb48 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Key do
     end
 
     context 'when entry value is not correct' do
-      let(:config) { [ 'incorrect' ] }
+      let(:config) { ['incorrect'] }
 
       describe '#errors' do
         it 'saves errors' do
@@ -31,4 +31,10 @@ describe Gitlab::Ci::Config::Entry::Key do
       end
     end
   end
+
+  describe '.default' do
+    it 'returns default key' do
+      expect(described_class.default).to eq 'default'
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/config/entry/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
index e60c9aaf661747a059f857915de6c5d6419de674..1d9c5ddee9b952b471d156c3ee591a4269050b1d 100644
--- a/spec/lib/gitlab/ci/config/entry/paths_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Paths do
     end
 
     context 'when entry value is not valid' do
-      let(:config) { [ 1 ] }
+      let(:config) { [1] }
 
       describe '#errors' do
         it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
index aa99cee26905fe3cbbd8ee9fbd53a83d5c16466e..069eaa264227915ab436a90e6362f630876d640b 100644
--- a/spec/lib/gitlab/ci/config/entry/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Config::Entry::Script do
 
   describe 'validations' do
     context 'when entry config value is correct' do
-      let(:config) { ['ls', 'pwd'] }
+      let(:config) { %w(ls pwd) }
 
       describe '#value' do
         it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 58327d089046a74eccb0ba7f2fb00e2616b1ef1a..f15f02f403edfda32f44bfd783b06692e895add4 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Variables do
     end
 
     context 'when entry value is not correct' do
-      let(:config) { [ :VAR, 'test' ] }
+      let(:config) { [:VAR, 'test'] }
 
       describe '#errors' do
         it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 0c40fca0c1a649186c5ecb7d2c869144270ae369..8b3bd08cf138f98d0908f07e63a7bb3743f8ee55 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -192,7 +192,7 @@ describe Gitlab::Ci::Status::Build::Factory do
       let(:build) { create(:ci_build, :playable) }
 
       it 'matches correct core status' do
-        expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+        expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
       end
 
       it 'matches correct extended statuses' do
@@ -200,12 +200,13 @@ describe Gitlab::Ci::Status::Build::Factory do
           .to eq [Gitlab::Ci::Status::Build::Play]
       end
 
-      it 'fabricates a core skipped status' do
+      it 'fabricates a play detailed status' do
         expect(status).to be_a Gitlab::Ci::Status::Build::Play
       end
 
       it 'fabricates status with correct details' do
         expect(status.text).to eq 'manual'
+        expect(status.group).to eq 'manual'
         expect(status.icon).to eq 'icon_status_manual'
         expect(status.label).to eq 'manual play action'
         expect(status).to have_details
@@ -218,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
       let(:build) { create(:ci_build, :playable, :teardown_environment) }
 
       it 'matches correct core status' do
-        expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+        expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
       end
 
       it 'matches correct extended statuses' do
@@ -226,12 +227,13 @@ describe Gitlab::Ci::Status::Build::Factory do
           .to eq [Gitlab::Ci::Status::Build::Stop]
       end
 
-      it 'fabricates a core skipped status' do
+      it 'fabricates a stop detailed status' do
         expect(status).to be_a Gitlab::Ci::Status::Build::Stop
       end
 
       it 'fabricates status with correct details' do
         expect(status.text).to eq 'manual'
+        expect(status.group).to eq 'manual'
         expect(status.icon).to eq 'icon_status_manual'
         expect(status.label).to eq 'manual stop action'
         expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f3e72ea1796b64c8c914bf84de83b951d64b0637..6c97a4fe5cad435cd030937662c42c9041c71f71 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -6,22 +6,10 @@ describe Gitlab::Ci::Status::Build::Play do
 
   subject { described_class.new(status) }
 
-  describe '#text' do
-    it { expect(subject.text).to eq 'manual' }
-  end
-
   describe '#label' do
     it { expect(subject.label).to eq 'manual play action' }
   end
 
-  describe '#icon' do
-    it { expect(subject.icon).to eq 'icon_status_manual' }
-  end
-
-  describe '#group' do
-    it { expect(subject.group).to eq 'manual' }
-  end
-
   describe 'action details' do
     let(:user) { create(:user) }
     let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 41c2b6247745805650199aa9b874fd2959e1050b..8d021c35a69c759f32935cdcba69895873c1791c 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -8,22 +8,10 @@ describe Gitlab::Ci::Status::Build::Stop do
     described_class.new(status)
   end
 
-  describe '#text' do
-    it { expect(subject.text).to eq 'manual' }
-  end
-
   describe '#label' do
     it { expect(subject.label).to eq 'manual stop action' }
   end
 
-  describe '#icon' do
-    it { expect(subject.icon).to eq 'icon_status_manual' }
-  end
-
-  describe '#group' do
-    it { expect(subject.group).to eq 'manual' }
-  end
-
   describe 'action details' do
     let(:user) { create(:user) }
     let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 38412fe2e4f67fff2e82ae5d76043707e6bf0359..768f8926f1d31074505f09a224b3640d1b3863ad 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Canceled do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'canceled' }
+    it { expect(subject.text).to eq 'canceled' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 6d847484693ff4feaec7e8898759c26eb574b12d..e96c13aede38b54840ada4bc41fdde51982e95c3 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Created do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'created' }
+    it { expect(subject.text).to eq 'created' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 990d686d22cdf91b546706e244b2357e5dbf78be..e5da0a91159a0c21f790b636cd06c83dd94968ca 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Failed do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'failed' }
+    it { expect(subject.text).to eq 'failed' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3fd3727b92ddf27e110160846f8896ed2672df74
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Manual do
+  subject do
+    described_class.new(double('subject'), double('user'))
+  end
+
+  describe '#text' do
+    it { expect(subject.text).to eq 'manual' }
+  end
+
+  describe '#label' do
+    it { expect(subject.label).to eq 'manual action' }
+  end
+
+  describe '#icon' do
+    it { expect(subject.icon).to eq 'icon_status_manual' }
+  end
+
+  describe '#group' do
+    it { expect(subject.group).to eq 'manual' }
+  end
+end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 7bb6579c317c88a8df68efd837dd8d552722bfb3..8d09cf2a05a13ccce498ded734cc1f34e1967a4b 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Pending do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'pending' }
+    it { expect(subject.text).to eq 'pending' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a2b952d3740d08bc22f8b87ba2e447721520bbc
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pipeline::Blocked do
+  let(:pipeline) { double('pipeline') }
+
+  subject do
+    described_class.new(pipeline)
+  end
+
+  describe '#text' do
+    it 'overrides status text' do
+      expect(subject.text).to eq 'blocked'
+    end
+  end
+
+  describe '#label' do
+    it 'overrides status label' do
+      expect(subject.label).to eq 'waiting for manual action'
+    end
+  end
+
+  describe '.matches?' do
+    let(:user) { double('user') }
+    subject { described_class.matches?(pipeline, user) }
+
+    context 'when pipeline is blocked' do
+      let(:pipeline) { create(:ci_pipeline, :blocked) }
+
+      it 'is a correct match' do
+        expect(subject).to be true
+      end
+    end
+
+    context 'when pipeline is not blocked' do
+      let(:pipeline) { create(:ci_pipeline, :success) }
+
+      it 'does not match' do
+        expect(subject).to be false
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index b10a447c27a8dade794b3dbfcd06368937e57834..dd754b849b22456a1c152434f80d2ddaf0daf1be 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
   end
 
   context 'when pipeline has a core status' do
-    HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+    (HasStatus::AVAILABLE_STATUSES - [HasStatus::BLOCKED_STATUS])
+      .each do |simple_status|
       context "when core status is #{simple_status}" do
         let(:pipeline) { create(:ci_pipeline, status: simple_status) }
 
@@ -23,7 +24,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
           expect(factory.core_status).to be_a expected_status
         end
 
-        it 'does not matche extended statuses' do
+        it 'does not match extended statuses' do
           expect(factory.extended_statuses).to be_empty
         end
 
@@ -39,6 +40,27 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
         end
       end
     end
+
+    context "when core status is manual" do
+      let(:pipeline) { create(:ci_pipeline, status: :manual) }
+
+      it "matches manual core status" do
+        expect(factory.core_status)
+          .to be_a Gitlab::Ci::Status::Manual
+      end
+
+      it 'matches a correct extended statuses' do
+        expect(factory.extended_statuses)
+          .to eq [Gitlab::Ci::Status::Pipeline::Blocked]
+      end
+
+      it 'extends core status with common pipeline methods' do
+        expect(status).to have_details
+        expect(status).not_to have_action
+        expect(status.details_path)
+          .to include "pipelines/#{pipeline.id}"
+      end
+    end
   end
 
   context 'when pipeline has warnings' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 852d6c06baf7db1125ca8e4e547b5b333aca1854..10d3bf749c1857c6f521fa5fd8b2bed6f81a2205 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Running do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'running' }
+    it { expect(subject.text).to eq 'running' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index e00b356a24b6e19711a6a55579a881e91daa24ef..10db93d38025604eca3bb4d80d66f982aacc8ed5 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Skipped do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'skipped' }
+    it { expect(subject.text).to eq 'skipped' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 4a89e1faf4043e4bdd26cd1456a8876902237cfd..230f24b94a4418fa0e528704adb8096efdef17dd 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Success do
   end
 
   describe '#text' do
-    it { expect(subject.label).to eq 'passed' }
+    it { expect(subject.text).to eq 'passed' }
   end
 
   describe '#label' do
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index fbf679c521554e727272edb59ccb3274dd4fdc0e..780ac0ad97ed433e8505c4f3ffd9c8138303b90e 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Conflict::File, lib: true do
 
       it 'returns a file containing only the chosen parts of the resolved sections' do
         expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
-          to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+          to eq(%w(both new both old both new both))
       end
     end
 
@@ -123,7 +123,7 @@ describe Gitlab::Conflict::File, lib: true do
     it 'sets conflict to true for sections with only changed lines' do
       conflict_file.sections.select { |section| section[:conflict] }.each do |section|
         section[:lines].each do |line|
-          expect(line.type).to be_in(['new', 'old'])
+          expect(line.type).to be_in(%w(new old))
         end
       end
     end
@@ -251,7 +251,7 @@ FILE
   describe '#as_json' do
     it 'includes the blob path for the file' do
       expect(conflict_file.as_json[:blob_path]).
-        to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+        to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb")
     end
 
     it 'includes the blob icon for the file' do
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index 16eb376635644762dcb29f047b5a3149161f33f1..2570f95dd21be4bc396c4df1feb44e7233f697ae 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -120,43 +120,61 @@ CONFLICT
     end
 
     context 'when the file contents include conflict delimiters' do
-      it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
-        expect { parse_text('=======') }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
-        expect { parse_text('>>>>>>> README.md') }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
-        expect { parse_text('>>>>>>> some-other-path.md') }.
-          not_to raise_error
+      context 'when there is a non-start delimiter first' do
+        it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
+          expect { parse_text('=======') }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
+
+        it 'raises UnexpectedDelimiter when there is an end delimiter first' do
+          expect { parse_text('>>>>>>> README.md') }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
+
+        it 'does not raise when there is an end delimiter for a different path first' do
+          expect { parse_text('>>>>>>> some-other-path.md') }.
+            not_to raise_error
+        end
       end
 
-      it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
-        start_text = "<<<<<<< README.md\n"
-        end_text = "\n=======\n>>>>>>> README.md"
+      context 'when a start delimiter is followed by a non-middle delimiter' do
+        let(:start_text) { "<<<<<<< README.md\n" }
+        let(:end_text) { "\n=======\n>>>>>>> README.md" }
 
-        expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
+          expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
 
-        expect { parse_text(start_text + start_text + end_text) }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
+          expect { parse_text(start_text + start_text + end_text) }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
 
-        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
-          not_to raise_error
+        it 'does not raise when it is followed by a start delimiter for a different path' do
+          expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+            not_to raise_error
+        end
       end
 
-      it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
-        start_text = "<<<<<<< README.md\n=======\n"
-        end_text = "\n>>>>>>> README.md"
+      context 'when a middle delimiter is followed by a non-end delimiter' do
+        let(:start_text) { "<<<<<<< README.md\n=======\n" }
+        let(:end_text) { "\n>>>>>>> README.md" }
 
-        expect { parse_text(start_text + '=======' + end_text) }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
+          expect { parse_text(start_text + '=======' + end_text) }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
 
-        expect { parse_text(start_text + start_text + end_text) }.
-          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
+          expect { parse_text(start_text + start_text + end_text) }.
+            to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+        end
 
-        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
-          not_to raise_error
+        it 'does not raise when it is followed by a start delimiter for another path' do
+          expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }.
+            not_to raise_error
+        end
       end
 
       it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
@@ -184,9 +202,20 @@ CONFLICT
           to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
       end
 
-      it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
-        expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
-          to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+      # All text from Rugged has an encoding of ASCII_8BIT, so force that in
+      # these strings.
+      context 'when the file contains UTF-8 characters' do
+        it 'does not raise' do
+          expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }.
+            not_to raise_error
+        end
+      end
+
+      context 'when the file contains non-UTF-8 characters' do
+        it 'raises UnsupportedEncoding' do
+          expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+            to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+        end
       end
     end
   end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index f01c42aff91a692464ca42b0b9a033e0bf837c9d..4ce4e6e10346a976c5b5323a961ab4afdc76d323 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,10 +1,16 @@
 require 'spec_helper'
 
-class MigrationTest
-  include Gitlab::Database
-end
-
 describe Gitlab::Database, lib: true do
+  before do
+    stub_const('MigrationTest', Class.new { include Gitlab::Database })
+  end
+
+  describe '.config' do
+    it 'returns a Hash' do
+      expect(described_class.config).to be_an_instance_of(Hash)
+    end
+  end
+
   describe '.adapter_name' do
     it 'returns the name of the adapter' do
       expect(described_class.adapter_name).to be_an_instance_of(String)
@@ -119,9 +125,24 @@ describe Gitlab::Database, lib: true do
     it 'creates a new connection pool with specific pool size' do
       pool = described_class.create_connection_pool(5)
 
-      expect(pool)
-        .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
-      expect(pool.spec.config[:pool]).to eq(5)
+      begin
+        expect(pool)
+          .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
+
+        expect(pool.spec.config[:pool]).to eq(5)
+      ensure
+        pool.disconnect!
+      end
+    end
+
+    it 'allows setting of a custom hostname' do
+      pool = described_class.create_connection_pool(5, '127.0.0.1')
+
+      begin
+        expect(pool.spec.config[:host]).to eq('127.0.0.1')
+      ensure
+        pool.disconnect!
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 5893485634d326e7e66e206bd88011ac29c471c4..c6bd4e81f4f507799a8b6bffd6728f3dc6f9abd5 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
       end
 
       it 'highlights and marks unchanged lines' do
-        code = %Q{ <span id="LC7" class="line">  <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
+        code = %Q{ <span id="LC7" class="line" lang="ruby">  <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
 
         expect(subject[2].text).to eq(code)
       end
 
       it 'highlights and marks removed lines' do
-        code = %Q{-<span id="LC9" class="line">      <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+        code = %Q{-<span id="LC9" class="line" lang="ruby">      <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
 
         expect(subject[4].text).to eq(code)
       end
 
       it 'highlights and marks added lines' do
-        code = %Q{+<span id="LC9" class="line">      <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+        code = %Q{+<span id="LC9" class="line" lang="ruby">      <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
 
         expect(subject[5].text).to eq(code)
       end
@@ -53,21 +53,21 @@ describe Gitlab::Diff::Highlight, lib: true do
       end
 
       it 'marks unchanged lines' do
-        code = %Q{   def popen(cmd, path=nil)}
+        code = %q{   def popen(cmd, path=nil)}
 
         expect(subject[2].text).to eq(code)
         expect(subject[2].text).not_to be_html_safe
       end
 
       it 'marks removed lines' do
-        code = %Q{-      raise "System commands must be given as an array of strings"}
+        code = %q{-      raise "System commands must be given as an array of strings"}
 
         expect(subject[4].text).to eq(code)
         expect(subject[4].text).not_to be_html_safe
       end
 
       it 'marks added lines' do
-        code = %Q{+      raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+        code = %q{+      raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
 
         expect(subject[5].text).to eq(code)
         expect(subject[5].text).to be_html_safe
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index b983d73f8be1df6b91cffd40ec9132dcdc8915d8..e76128ecd87ab9eb3fb5e3a08b6601bb78c06e5c 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -91,6 +91,54 @@ eos
     end
   end
 
+  describe '\ No newline at end of file' do
+    it "parses nonewline in one file correctly" do
+      first_nonewline_diff = <<~END
+        --- a/test
+        +++ b/test
+        @@ -1,2 +1,2 @@
+        +ipsum
+         lorem
+        -ipsum
+        \\ No newline at end of file
+      END
+      lines = parser.parse(first_nonewline_diff.lines).to_a
+
+      expect(lines[0].type).to eq('new')
+      expect(lines[0].text).to eq('+ipsum')
+      expect(lines[2].type).to eq('old')
+      expect(lines[3].type).to eq('old-nonewline')
+      expect(lines[1].old_pos).to eq(1)
+      expect(lines[1].new_pos).to eq(2)
+    end
+
+    it "parses nonewline in two files correctly" do
+      both_nonewline_diff = <<~END
+        --- a/test
+        +++ b/test
+        @@ -1,2 +1,2 @@
+        -lorem
+        -ipsum
+        \\ No newline at end of file
+        +ipsum
+        +lorem
+        \\ No newline at end of file
+      END
+      lines = parser.parse(both_nonewline_diff.lines).to_a
+
+      expect(lines[0].type).to eq('old')
+      expect(lines[1].type).to eq('old')
+      expect(lines[2].type).to eq('old-nonewline')
+      expect(lines[5].type).to eq('new-nonewline')
+      expect(lines[3].text).to eq('+ipsum')
+      expect(lines[3].old_pos).to eq(3)
+      expect(lines[3].new_pos).to eq(1)
+      expect(lines[4].text).to eq('+lorem')
+      expect(lines[4].old_pos).to eq(3)
+      expect(lines[4].new_pos).to eq(2)
+    end
+  end
+
   context 'when lines is empty' do
     it { expect(parser.parse([])).to eq([]) }
     it { expect(parser.parse(nil)).to eq([]) }
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b5bfc4dbb00d6783aa5e5d8fdbe2437e0ecc5ea
--- /dev/null
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+
+describe Gitlab::EtagCaching::Middleware do
+  let(:app) { double(:app) }
+  let(:middleware) { described_class.new(app) }
+  let(:app_status_code) { 200 }
+  let(:if_none_match) { nil }
+  let(:enabled_path) { '/gitlab-org/gitlab-ce/noteable/issue/1/notes' }
+
+  context 'when ETag caching is not enabled for current route' do
+    let(:path) { '/gitlab-org/gitlab-ce/tree/master/noteable/issue/1/notes' }
+
+    before do
+      mock_app_response
+    end
+
+    it 'does not add ETag header' do
+      _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(headers['ETag']).to be_nil
+    end
+
+    it 'passes status code from app' do
+      status, _, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(status).to eq app_status_code
+    end
+  end
+
+  context 'when there is no ETag in store for given resource' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_app_response
+      mock_value_in_store(nil)
+    end
+
+    it 'generates ETag' do
+      expect_any_instance_of(Gitlab::EtagCaching::Store)
+        .to receive(:touch).and_return('123')
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    context 'when If-None-Match header was specified' do
+      let(:if_none_match) { 'W/"abc"' }
+
+      it 'tracks "etag_caching_key_not_found" event' do
+        expect(Gitlab::Metrics).to receive(:add_event)
+          .with(:etag_caching_middleware_used)
+        expect(Gitlab::Metrics).to receive(:add_event)
+          .with(:etag_caching_key_not_found)
+
+        middleware.call(build_env(path, if_none_match))
+      end
+    end
+  end
+
+  context 'when there is ETag in store for given resource' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_app_response
+      mock_value_in_store('123')
+    end
+
+    it 'returns this value as header' do
+      _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(headers['ETag']).to eq 'W/"123"'
+    end
+  end
+
+  context 'when If-None-Match header matches ETag in store' do
+    let(:path) { enabled_path }
+    let(:if_none_match) { 'W/"123"' }
+
+    before do
+      mock_value_in_store('123')
+    end
+
+    it 'does not call app' do
+      expect(app).not_to receive(:call)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    it 'returns status code 304' do
+      status, _, _ = middleware.call(build_env(path, if_none_match))
+
+      expect(status).to eq 304
+    end
+
+    it 'tracks "etag_caching_cache_hit" event' do
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_cache_hit)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  context 'when If-None-Match header does not match ETag in store' do
+    let(:path) { enabled_path }
+    let(:if_none_match) { 'W/"abc"' }
+
+    before do
+      mock_value_in_store('123')
+    end
+
+    it 'calls app' do
+      expect(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+
+      middleware.call(build_env(path, if_none_match))
+    end
+
+    it 'tracks "etag_caching_resource_changed" event' do
+      mock_app_response
+
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_resource_changed)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  context 'when If-None-Match header is not specified' do
+    let(:path) { enabled_path }
+
+    before do
+      mock_value_in_store('123')
+      mock_app_response
+    end
+
+    it 'tracks "etag_caching_header_missing" event' do
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_middleware_used)
+      expect(Gitlab::Metrics).to receive(:add_event)
+        .with(:etag_caching_header_missing)
+
+      middleware.call(build_env(path, if_none_match))
+    end
+  end
+
+  def mock_app_response
+    allow(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+  end
+
+  def mock_value_in_store(value)
+    allow_any_instance_of(Gitlab::EtagCaching::Store)
+      .to receive(:get).and_return(value)
+  end
+
+  def build_env(path, if_none_match)
+    {
+      'PATH_INFO' => path,
+      'HTTP_IF_NONE_MATCH' => if_none_match
+    }
+  end
+end
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
index 79b1311ac916fdbb3d074e48dd2807719e213639..17d6be470aca5635eeb898c4414d96707ab81a86 100644
--- a/spec/lib/gitlab/git/blob_snippet_spec.rb
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Git::BlobSnippet, seed_helper: true do
     end
 
     context 'present lines' do
-      let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
+      let(:snippet) { Gitlab::Git::BlobSnippet.new('master', %w(wow much), 1, 'wow.rb') }
 
       it { expect(snippet.data).to eq("wow\nmuch") }
     end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 84f79ec2391bba4bd36514efc57577920b1eb61c..8049e2c120d6743755c10e3fc108c10da2790870 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
     end
   end
 
-  describe :commit do
-    let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
-
-    let(:commit_options) do
-      {
-         file: {
-           content: 'Lorem ipsum...',
-           path: 'documents/story.txt'
-         },
-         author: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         committer: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         commit: {
-           message: 'Wow such commit',
-           branch: 'fix-mode'
-         }
-      }
-    end
-
-    let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
-    let(:commit) { repository.lookup(commit_sha) }
-
-    it 'should add file with commit' do
-      # Commit message valid
-      expect(commit.message).to eq('Wow such commit')
-
-      tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
-
-      # Directory was created
-      expect(tree[:type]).to eq(:tree)
-
-      # File was created
-      expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
-    end
-
-    describe "ref updating" do
-      it 'creates a commit but does not udate a ref' do
-        commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
-        commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
-        commit = repository.lookup(commit_sha)
-
-        # Commit message valid
-        expect(commit.message).to eq('Wow such commit')
-
-        # Does not update any related ref
-        expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
-        expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
-      end
-    end
-
-    describe 'reject updates' do
-      it 'should reject updates' do
-        commit_options[:file][:update] = false
-        commit_options[:file][:path] = 'files/executables/ls'
-
-        expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
-      end
-    end
-
-    describe 'file modes' do
-      it 'should preserve file modes with commit' do
-        commit_options[:file][:path] = 'files/executables/ls'
-
-        entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
-        expect(entry[:filemode]).to eq(0100755)
-      end
-    end
-  end
-
-  describe :rename do
-    let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
-    let(:rename_options) do
-      {
-        file: {
-          path: 'NEWCONTRIBUTING.md',
-          previous_path: 'CONTRIBUTING.md',
-          content: 'Lorem ipsum...',
-          update: true
-        },
-        author: {
-          email: 'user@example.com',
-          name: 'Test User',
-          time: Time.now
-        },
-        committer: {
-          email: 'user@example.com',
-          name: 'Test User',
-          time: Time.now
-        },
-        commit: {
-          message: 'Rename readme',
-          branch: 'master'
-        }
-      }
-    end
-
-    let(:rename_options2) do
-      {
-         file: {
-           content: 'Lorem ipsum...',
-           path: 'bin/new_executable',
-           previous_path: 'bin/executable',
-         },
-         author: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         committer: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         commit: {
-           message: 'Updates toberenamed.txt',
-           branch: 'master',
-           update_ref: false
-         }
-      }
-    end
-
-    it 'maintains file permissions when renaming' do
-      mode = 0o100755
-
-      Gitlab::Git::Blob.rename(repository, rename_options2)
-
-      expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
-    end
-
-    it 'renames the file with commit and not change file permissions' do
-      ref = rename_options[:commit][:branch]
-
-      expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
-      expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
-
-      expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
-      expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
-    end
-  end
-
-  describe :remove do
-    let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
-
-    let(:commit_options) do
-      {
-         file: {
-           path: 'README.md'
-         },
-         author: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         committer: {
-           email: 'user@example.com',
-           name: 'Test User',
-           time: Time.now
-         },
-         commit: {
-           message: 'Remove readme',
-           branch: 'feature'
-         }
-      }
-    end
-
-    let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
-    let(:commit) { repository.lookup(commit_sha) }
-    let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
-
-    it 'should remove file with commit' do
-      # Commit message valid
-      expect(commit.message).to eq('Remove readme')
-
-      # File was removed
-      expect(blob).to be_nil
-    end
-  end
-
   describe :lfs_pointers do
     context 'file a valid lfs pointer' do
       let(:blob) do
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 4fa72c565aece98a15cd5368521bf9c10acae903..47bdd7310d5c79c0d07af6b989c745e3f54f20b1 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -365,7 +365,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
         end
 
         context 'when go over safe limits on files' do
-          let(:iterator) { [ fake_diff(1, 1) ] * 4 }
+          let(:iterator) { [fake_diff(1, 1)] * 4 }
 
           before(:each) do
             stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4c55532d165f7461c2a5648a6d02af2b08b69687..992126ef153b9ecc6fe24f4f12bb530d044da244 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -109,6 +109,43 @@ EOT
         end
       end
     end
+
+    context 'using a Gitaly::CommitDiffResponse' do
+      let(:diff) do
+        described_class.new(
+          Gitaly::CommitDiffResponse.new(
+            to_path: ".gitmodules",
+            from_path: ".gitmodules",
+            old_mode: 0100644,
+            new_mode: 0100644,
+            from_id: '357406f3075a57708d0163752905cc1576fceacc',
+            to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+            raw_chunks: raw_chunks,
+          )
+        )
+      end
+
+      context 'with a small diff' do
+        let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+
+        it 'initializes the diff' do
+          expect(diff.to_hash).to eq(@raw_diff_hash)
+        end
+
+        it 'does not prune the diff' do
+          expect(diff).not_to be_too_large
+        end
+      end
+
+      context 'using a diff that is too large' do
+        let(:raw_chunks) { ['a' * 204800] }
+
+        it 'prunes the diff' do
+          expect(diff.diff).to be_empty
+          expect(diff).to be_too_large
+        end
+      end
+    end
   end
 
   describe 'straight diffs' do
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d0c7ca60ddc42c482110bae279b8047c1bbdee2f
--- /dev/null
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -0,0 +1,220 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Index, seed_helper: true do
+  let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+  let(:index) { described_class.new(repository) }
+
+  before do
+    index.read_tree(repository.lookup('master').tree)
+  end
+
+  describe '#create' do
+    let(:options) do
+      {
+        content: 'Lorem ipsum...',
+        file_path: 'documents/story.txt'
+      }
+    end
+
+    context 'when no file at that path exists' do
+      it 'creates the file in the index' do
+        index.create(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(entry).not_to be_nil
+        expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+      end
+    end
+
+    context 'when a file at that path exists' do
+      before do
+        options[:file_path] = 'files/executables/ls'
+      end
+
+      it 'raises an error' do
+        expect { index.create(options) }.to raise_error('Filename already exists')
+      end
+    end
+
+    context 'when content is in base64' do
+      before do
+        options[:content] = Base64.encode64(options[:content])
+        options[:encoding] = 'base64'
+      end
+
+      it 'decodes base64' do
+        index.create(options)
+
+        entry = index.get(options[:file_path])
+        expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content]))
+      end
+    end
+
+    context 'when content contains CRLF' do
+      before do
+        repository.autocrlf = :input
+        options[:content] = "Hello,\r\nWorld"
+      end
+
+      it 'converts to LF' do
+        index.create(options)
+
+        entry = index.get(options[:file_path])
+        expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld")
+      end
+    end
+  end
+
+  describe '#create_dir' do
+    let(:options) do
+      {
+        file_path: 'newdir'
+      }
+    end
+
+    context 'when no file or dir at that path exists' do
+      it 'creates the dir in the index' do
+        index.create_dir(options)
+
+        entry = index.get(options[:file_path] + '/.gitkeep')
+
+        expect(entry).not_to be_nil
+      end
+    end
+
+    context 'when a file at that path exists' do
+      before do
+        options[:file_path] = 'files/executables/ls'
+      end
+
+      it 'raises an error' do
+        expect { index.create_dir(options) }.to raise_error('Directory already exists as a file')
+      end
+    end
+
+    context 'when a directory at that path exists' do
+      before do
+        options[:file_path] = 'files/executables'
+      end
+
+      it 'raises an error' do
+        expect { index.create_dir(options) }.to raise_error('Directory already exists')
+      end
+    end
+  end
+
+  describe '#update' do
+    let(:options) do
+      {
+        content: 'Lorem ipsum...',
+        file_path: 'README.md'
+      }
+    end
+
+    context 'when no file at that path exists' do
+      before do
+        options[:file_path] = 'documents/story.txt'
+      end
+
+      it 'raises an error' do
+        expect { index.update(options) }.to raise_error("File doesn't exist")
+      end
+    end
+
+    context 'when a file at that path exists' do
+      it 'updates the file in the index' do
+        index.update(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+      end
+
+      it 'preserves file mode' do
+        options[:file_path] = 'files/executables/ls'
+
+        index.update(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(entry[:mode]).to eq(0100755)
+      end
+    end
+  end
+
+  describe '#move' do
+    let(:options) do
+      {
+        content: 'Lorem ipsum...',
+        previous_path: 'README.md',
+        file_path: 'NEWREADME.md'
+      }
+    end
+
+    context 'when no file at that path exists' do
+      it 'raises an error' do
+        options[:previous_path] = 'documents/story.txt'
+
+        expect { index.move(options) }.to raise_error("File doesn't exist")
+      end
+    end
+
+    context 'when a file at that path exists' do
+      it 'removes the old file in the index' do
+        index.move(options)
+
+        entry = index.get(options[:previous_path])
+
+        expect(entry).to be_nil
+      end
+
+      it 'creates the new file in the index' do
+        index.move(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(entry).not_to be_nil
+        expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+      end
+
+      it 'preserves file mode' do
+        options[:previous_path] = 'files/executables/ls'
+
+        index.move(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(entry[:mode]).to eq(0100755)
+      end
+    end
+  end
+
+  describe '#delete' do
+    let(:options) do
+      {
+        file_path: 'README.md'
+      }
+    end
+
+    context 'when no file at that path exists' do
+      before do
+        options[:file_path] = 'documents/story.txt'
+      end
+
+      it 'raises an error' do
+        expect { index.delete(options) }.to raise_error("File doesn't exist")
+      end
+    end
+
+    context 'when a file at that path exists' do
+      it 'removes the file in the index' do
+        index.delete(options)
+
+        entry = index.get(options[:file_path])
+
+        expect(entry).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 2a915bf426f9849542c99f06b347f8e82e915fc5..9ed43da1116a8fdc52679f08071db9fb62c6989c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -47,7 +47,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  describe :branch_names do
+  describe '#branch_names' do
     subject { repository.branch_names }
 
     it 'has SeedRepo::Repo::BRANCHES.size elements' do
@@ -57,7 +57,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     it { is_expected.not_to include("branch-from-space") }
   end
 
-  describe :tag_names do
+  describe '#tag_names' do
     subject { repository.tag_names }
 
     it { is_expected.to be_kind_of Array }
@@ -78,49 +78,69 @@ describe Gitlab::Git::Repository, seed_helper: true do
     it { expect(metadata['ArchivePath']).to end_with extenstion }
   end
 
-  describe :archive do
+  describe '#archive_prefix' do
+    let(:project_name) { 'project-name'}
+
+    before do
+      expect(repository).to receive(:name).once.and_return(project_name)
+    end
+
+    it 'returns parameterised string for a ref containing slashes' do
+      prefix = repository.archive_prefix('test/branch', 'SHA')
+
+      expect(prefix).to eq("#{project_name}-test-branch-SHA")
+    end
+
+    it 'returns correct string for a ref containing dots' do
+      prefix = repository.archive_prefix('test.branch', 'SHA')
+
+      expect(prefix).to eq("#{project_name}-test.branch-SHA")
+    end
+  end
+
+  describe '#archive' do
     let(:metadata) { repository.archive_metadata('master', '/tmp') }
 
     it_should_behave_like 'archive check', '.tar.gz'
   end
 
-  describe :archive_zip do
+  describe '#archive_zip' do
     let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') }
 
     it_should_behave_like 'archive check', '.zip'
   end
 
-  describe :archive_bz2 do
+  describe '#archive_bz2' do
     let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') }
 
     it_should_behave_like 'archive check', '.tar.bz2'
   end
 
-  describe :archive_fallback do
+  describe '#archive_fallback' do
     let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') }
 
     it_should_behave_like 'archive check', '.tar.gz'
   end
 
-  describe :size do
+  describe '#size' do
     subject { repository.size }
 
     it { is_expected.to be < 2 }
   end
 
-  describe :has_commits? do
+  describe '#has_commits?' do
     it { expect(repository.has_commits?).to be_truthy }
   end
 
-  describe :empty? do
+  describe '#empty?' do
     it { expect(repository.empty?).to be_falsey }
   end
 
-  describe :bare? do
+  describe '#bare?' do
     it { expect(repository.bare?).to be_truthy }
   end
 
-  describe :heads do
+  describe '#heads' do
     let(:heads) { repository.heads }
     subject { heads }
 
@@ -147,7 +167,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  describe :ref_names do
+  describe '#ref_names' do
     let(:ref_names) { repository.ref_names }
     subject { ref_names }
 
@@ -164,7 +184,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  describe :search_files do
+  describe '#search_files' do
     let(:results) { repository.search_files('rails', 'master') }
     subject { results }
 
@@ -200,7 +220,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  context :submodules do
+  context '#submodules' do
     let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
 
     context 'where repo has submodules' do
@@ -264,7 +284,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  describe :commit_count do
+  describe '#commit_count' do
     it { expect(repository.commit_count("master")).to eq(25) }
     it { expect(repository.commit_count("feature")).to eq(9) }
   end
@@ -493,7 +513,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
   describe "#remote_add" do
     before(:all) do
       @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
-      @repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
+      @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
     end
 
     it "should add the remote" do
@@ -529,7 +549,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     commit_with_new_name = nil
     rename_commit = nil
 
-    before(:all) do
+    before(:context) do
       # Add new commits so that there's a renamed file in the commit history
       repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
 
@@ -538,49 +558,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
       commit_with_new_name = new_commit_edit_new_file(repo)
     end
 
+    after(:context) do
+      # Erase our commits so other tests get the original repo
+      repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+      repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+    end
+
     context "where 'follow' == true" do
-      options = { ref: "master", follow: true }
+      let(:options) { { ref: "master", follow: true } }
 
       context "and 'path' is a directory" do
-        let(:log_commits) do
-          repository.log(options.merge(path: "encoding"))
-        end
+        it "does not follow renames" do
+          log_commits = repository.log(options.merge(path: "encoding"))
 
-        it "should not follow renames" do
-          expect(log_commits).to include(commit_with_new_name)
-          expect(log_commits).to include(rename_commit)
-          expect(log_commits).not_to include(commit_with_old_name)
+          aggregate_failures do
+            expect(log_commits).to include(commit_with_new_name)
+            expect(log_commits).to include(rename_commit)
+            expect(log_commits).not_to include(commit_with_old_name)
+          end
         end
       end
 
       context "and 'path' is a file that matches the new filename" do
-        let(:log_commits) do
-          repository.log(options.merge(path: "encoding/CHANGELOG"))
+        context 'without offset' do
+          it "follows renames" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
+
+            aggregate_failures do
+              expect(log_commits).to include(commit_with_new_name)
+              expect(log_commits).to include(rename_commit)
+              expect(log_commits).to include(commit_with_old_name)
+            end
+          end
         end
 
-        it "should follow renames" do
-          expect(log_commits).to include(commit_with_new_name)
-          expect(log_commits).to include(rename_commit)
-          expect(log_commits).to include(commit_with_old_name)
+        context 'with offset=1' do
+          it "follows renames and skip the latest commit" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
+
+            aggregate_failures do
+              expect(log_commits).not_to include(commit_with_new_name)
+              expect(log_commits).to include(rename_commit)
+              expect(log_commits).to include(commit_with_old_name)
+            end
+          end
+        end
+
+        context 'with offset=1', 'and limit=1' do
+          it "follows renames, skip the latest commit and return only one commit" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
+
+            expect(log_commits).to contain_exactly(rename_commit)
+          end
+        end
+
+        context 'with offset=1', 'and limit=2' do
+          it "follows renames, skip the latest commit and return only two commits" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
+
+            aggregate_failures do
+              expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
+            end
+          end
+        end
+
+        context 'with offset=2' do
+          it "follows renames and skip the latest commit" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
+
+            aggregate_failures do
+              expect(log_commits).not_to include(commit_with_new_name)
+              expect(log_commits).not_to include(rename_commit)
+              expect(log_commits).to include(commit_with_old_name)
+            end
+          end
+        end
+
+        context 'with offset=2', 'and limit=1' do
+          it "follows renames, skip the two latest commit and return only one commit" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
+
+            expect(log_commits).to contain_exactly(commit_with_old_name)
+          end
+        end
+
+        context 'with offset=2', 'and limit=2' do
+          it "follows renames, skip the two latest commit and return only one commit" do
+            log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
+
+            aggregate_failures do
+              expect(log_commits).not_to include(commit_with_new_name)
+              expect(log_commits).not_to include(rename_commit)
+              expect(log_commits).to include(commit_with_old_name)
+            end
+          end
         end
       end
 
       context "and 'path' is a file that matches the old filename" do
-        let(:log_commits) do
-          repository.log(options.merge(path: "CHANGELOG"))
-        end
+        it "does not follow renames" do
+          log_commits = repository.log(options.merge(path: "CHANGELOG"))
 
-        it "should not follow renames" do
-          expect(log_commits).to include(commit_with_old_name)
-          expect(log_commits).to include(rename_commit)
-          expect(log_commits).not_to include(commit_with_new_name)
+          aggregate_failures do
+            expect(log_commits).not_to include(commit_with_new_name)
+            expect(log_commits).to include(rename_commit)
+            expect(log_commits).to include(commit_with_old_name)
+          end
         end
       end
 
       context "unknown ref" do
-        let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
+        it "returns an empty array" do
+          log_commits = repository.log(options.merge(ref: 'unknown'))
 
-        it "should return empty" do
           expect(log_commits).to eq([])
         end
       end
@@ -699,12 +789,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
         end
       end
     end
-
-    after(:all) do
-      # Erase our commits so other tests get the original repo
-      repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
-      repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
-    end
   end
 
   describe "#commits_between" do
@@ -746,6 +830,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
     it { is_expected.to eq(17) }
   end
 
+  describe '#count_commits' do
+    context 'with after timestamp' do
+      it 'returns the number of commits after timestamp' do
+        options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') }
+
+        expect(repository.count_commits(options)).to eq(25)
+      end
+    end
+
+    context 'with before timestamp' do
+      it 'returns the number of commits after timestamp' do
+        options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') }
+
+        expect(repository.count_commits(options)).to eq(9)
+      end
+    end
+
+    context 'with path' do
+      it 'returns the number of commits with path ' do
+        options = { ref: 'master', limit: nil, path: "encoding" }
+
+        expect(repository.count_commits(options)).to eq(2)
+      end
+    end
+  end
+
   describe "branch_names_contains" do
     subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
 
@@ -844,81 +954,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
     end
   end
 
-  describe '#mkdir' do
-    let(:commit_options) do
-      {
-        author: {
-          email: 'user@example.com',
-          name: 'Test User',
-          time: Time.now
-        },
-        committer: {
-          email: 'user@example.com',
-          name: 'Test User',
-          time: Time.now
-        },
-        commit: {
-          message: 'Test message',
-          branch: 'refs/heads/fix',
-        }
-      }
-    end
-
-    def generate_diff_for_path(path)
-      "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
-new file mode 100644
-index 0000000..e69de29
---- /dev/null
-+++ b/#{path}/.gitkeep\n"
-    end
-
-    shared_examples 'mkdir diff check' do |path, expected_path|
-      it 'creates a directory' do
-        result = repository.mkdir(path, commit_options)
-        expect(result).not_to eq(nil)
-
-        # Verify another mkdir doesn't create a directory that already exists
-        expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
-      end
-    end
-
-    describe 'creates a directory in root directory' do
-      it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
-    end
-
-    describe 'creates a directory in subdirectory' do
-      it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
-    end
-
-    describe 'creates a directory in subdirectory with a slash' do
-      it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
-    end
-
-    describe 'creates a directory in subdirectory with multiple slashes' do
-      it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
-    end
-
-    describe 'handles relative paths' do
-      it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
-    end
-
-    describe 'creates nested directories' do
-      it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
-    end
-
-    it 'does not attempt to create a directory with invalid relative path' do
-      expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
-    end
-
-    it 'does not attempt to overwrite a file' do
-      expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
-    end
-
-    it 'does not attempt to overwrite a directory' do
-      expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
-    end
-  end
-
   describe "#ls_files" do
     let(:master_file_paths) { repository.ls_files("master") }
     let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a55bd4387e0b20d6cb0169537b7e7f8d9d1e58cd..48f7754bed8fa483acf586b471a80466075dcf3e 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -199,7 +199,9 @@ describe Gitlab::GitAccess, lib: true do
 
     def stub_git_hooks
       # Running the `pre-receive` hook is expensive, and not necessary for this test.
-      allow_any_instance_of(GitHooksService).to receive(:execute).and_yield
+      allow_any_instance_of(GitHooksService).to receive(:execute) do |service, &block|
+        block.call(service)
+      end
     end
 
     def merge_into_protected_branch
@@ -207,13 +209,12 @@ describe Gitlab::GitAccess, lib: true do
         stub_git_hooks
         project.repository.add_branch(user, unprotected_branch, 'feature')
         target_branch = project.repository.lookup('feature')
-        source_branch = project.repository.commit_file(
+        source_branch = project.repository.create_file(
           user,
           FFaker::InternetSE.login_user_name,
           FFaker::HipsterIpsum.paragraph,
           message: FFaker::HipsterIpsum.sentence,
-          branch_name: unprotected_branch,
-          update: false)
+          branch_name: unprotected_branch)
         rugged = project.repository.rugged
         author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
 
@@ -232,11 +233,18 @@ describe Gitlab::GitAccess, lib: true do
             else
               project.team << [user, role]
             end
+          end
+
+          permissions_matrix[role].each do |action, allowed|
+            context action do
+              subject { access.send(:check_push_access!, changes[action]) }
 
-            permissions_matrix[role].each do |action, allowed|
-              context action do
-                subject { access.send(:check_push_access!, changes[action]) }
-                it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+              it do
+                if allowed
+                  expect { subject }.not_to raise_error
+                else
+                  expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
+                end
               end
             end
           end
@@ -301,7 +309,7 @@ describe Gitlab::GitAccess, lib: true do
       }
     }
 
-    [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+    [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
       context do
         before { create(:protected_branch, name: protected_branch_name, project: project) }
 
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 219198eff60f5d203ff0c89ff6246b861f407c8f..8eaf7aac264616fe9d6f40f53750ebd3162b092f 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Git, lib: true do
 
   describe 'committer_hash' do
     it "returns a hash containing the given email and name" do
-      committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+      committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: committer_name)
 
       expect(committer_hash[:email]).to eq(committer_email)
       expect(committer_hash[:name]).to eq(committer_name)
@@ -28,7 +28,7 @@ describe Gitlab::Git, lib: true do
 
     context 'when email is nil' do
       it "returns nil" do
-        committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+        committer_hash = Gitlab::Git.committer_hash(email: nil, name: committer_name)
 
         expect(committer_hash).to be_nil
       end
@@ -36,7 +36,7 @@ describe Gitlab::Git, lib: true do
 
     context 'when name is nil' do
       it "returns nil" do
-        committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+        committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: nil)
 
         expect(committer_hash).to be_nil
       end
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4684b1d1ac048816fb97ccf7a6f5ab653a29d90f
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Commit do
+  describe '.diff_from_parent' do
+    let(:diff_stub) { double('Gitaly::Diff::Stub') }
+    let(:project) { create(:project, :repository) }
+    let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
+    let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+
+    before do
+      allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
+      allow(diff_stub).to receive(:commit_diff).and_return([])
+    end
+
+    context 'when a commit has a parent' do
+      it 'sends an RPC request with the parent ID as left commit' do
+        request = Gitaly::CommitDiffRequest.new(
+          repository: repository_message,
+          left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+          right_commit_id: commit.id,
+        )
+
+        expect(diff_stub).to receive(:commit_diff).with(request)
+
+        described_class.diff_from_parent(commit)
+      end
+    end
+
+    context 'when a commit does not have a parent' do
+      it 'sends an RPC request with empty tree ref as left commit' do
+        initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+        request        = Gitaly::CommitDiffRequest.new(
+          repository: repository_message,
+          left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+          right_commit_id: initial_commit.id,
+        )
+
+        expect(diff_stub).to receive(:commit_diff).with(request)
+
+        described_class.diff_from_parent(initial_commit)
+      end
+    end
+
+    it 'returns a Gitlab::Git::DiffCollection' do
+      ret = described_class.diff_from_parent(commit)
+
+      expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
+    end
+
+    it 'passes options to Gitlab::Git::DiffCollection' do
+      options = { max_files: 31, max_lines: 13 }
+
+      expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+
+      described_class.diff_from_parent(commit, options)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a6252c99aa14819c2d6c3bc25c6c63e268060095
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Notifications do
+  let(:client) { Gitlab::GitalyClient::Notifications.new }
+
+  before do
+    allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
+  end
+
+  describe '#post_receive' do
+    let(:repo_path) { '/path/to/my_repo.git' }
+
+    it 'sends a post_receive message' do
+      expect_any_instance_of(Gitaly::Notifications::Stub).
+        to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path))
+
+      client.post_receive(repo_path)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index 36e7d739f7ec632a17db26508cc9391b9a6f06f0..3a31f93efa539a4655cc4a7d505efb13669535ec 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -6,27 +6,27 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
   let(:repo) { double }
   let(:raw) do
     {
-      ref: 'feature',
+      ref: 'branch-merged',
       repo: repo,
       sha: commit.id
     }
   end
 
   describe '#exists?' do
-    it 'returns true when both branch, and commit exists' do
+    it 'returns true when branch exists and commit is part of the branch' do
       branch = described_class.new(project, double(raw))
 
       expect(branch.exists?).to eq true
     end
 
-    it 'returns false when branch does not exist' do
-      branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
+    it 'returns false when branch exists and commit is not part of the branch' do
+      branch = described_class.new(project, double(raw.merge(ref: 'feature')))
 
       expect(branch.exists?).to eq false
     end
 
-    it 'returns false when commit does not exist' do
-      branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
+    it 'returns false when branch does not exist' do
+      branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
 
       expect(branch.exists?).to eq false
     end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 33d83d6d2f187ceee4e65c4ff9d30251f681ef64..8b867fbe322b303d367b19481d2c4997ea53deb4 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
       allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
     end
 
-    let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
-    let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
-    let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
     let(:label1) do
       double(
         name: 'Bug',
@@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
       )
     end
 
-    let!(:user) { create(:user, email: octocat.email) }
-    let(:repository) { double(id: 1, fork: false) }
-    let(:source_sha) { create(:commit, project: project).id }
-    let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
-    let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
-    let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
-    let(:pull_request) do
-      double(
-        number: 1347,
-        milestone: nil,
-        state: 'open',
-        title: 'New feature',
-        body: 'Please pull these awesome changes',
-        head: source_branch,
-        base: target_branch,
-        assignee: nil,
-        user: octocat,
-        created_at: created_at,
-        updated_at: updated_at,
-        closed_at: nil,
-        merged_at: nil,
-        url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
-        labels: [double(name: 'Label #2')]
-      )
-    end
-
     let(:release1) do
       double(
         tag_name: 'v1.0.0',
@@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do
       )
     end
 
+    subject { described_class.new(project) }
+
     it 'returns true' do
-      expect(described_class.new(project).execute).to eq true
+      expect(subject.execute).to eq true
     end
 
     it 'does not raise an error' do
-      expect { described_class.new(project).execute }.not_to raise_error
+      expect { subject.execute }.not_to raise_error
     end
 
     it 'stores error messages' do
@@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do
     end
   end
 
+  shared_examples 'Gitlab::GithubImport unit-testing' do
+    describe '#clean_up_restored_branches' do
+      subject { described_class.new(project) }
+
+      before do
+        allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
+        allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
+      end
+
+      context 'when pull request stills open' do
+        let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) }
+
+        it 'does not remove branches' do
+          expect(subject).not_to receive(:remove_branch)
+          subject.send(:clean_up_restored_branches, gh_pull_request)
+        end
+      end
+
+      context 'when pull request is closed' do
+        let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) }
+
+        it 'does remove branches' do
+          expect(subject).to receive(:remove_branch).at_least(2).times
+          subject.send(:clean_up_restored_branches, gh_pull_request)
+        end
+      end
+    end
+  end
+
   let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
+  let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
   let(:credentials) { { user: 'joe' } }
 
+  let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+  let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+  let(:repository) { double(id: 1, fork: false) }
+  let(:source_sha) { create(:commit, project: project).id }
+  let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
+  let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+  let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+  let(:pull_request) do
+    double(
+      number: 1347,
+      milestone: nil,
+      state: 'open',
+      title: 'New feature',
+      body: 'Please pull these awesome changes',
+      head: source_branch,
+      base: target_branch,
+      assignee: nil,
+      user: octocat,
+      created_at: created_at,
+      updated_at: updated_at,
+      closed_at: nil,
+      merged_at: nil,
+      url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+      labels: [double(name: 'Label #2')]
+    )
+  end
+  let(:closed_pull_request) do
+    double(
+      number: 1347,
+      milestone: nil,
+      state: 'closed',
+      title: 'New feature',
+      body: 'Please pull these awesome changes',
+      head: source_branch,
+      base: target_branch,
+      assignee: nil,
+      user: octocat,
+      created_at: created_at,
+      updated_at: updated_at,
+      closed_at: updated_at,
+      merged_at: nil,
+      url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+      labels: [double(name: 'Label #2')]
+    )
+  end
+
   context 'when importing a GitHub project' do
     let(:api_root) { 'https://api.github.com' }
     let(:repo_root) { 'https://github.com' }
+    subject { described_class.new(project) }
 
     it_behaves_like 'Gitlab::GithubImport::Importer#execute'
     it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+    it_behaves_like 'Gitlab::GithubImport unit-testing'
 
     describe '#client' do
       it 'instantiates a Client' do
@@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
           {}
         )
 
-        described_class.new(project).client
+        subject.client
       end
     end
   end
@@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
   context 'when importing a Gitea project' do
     let(:api_root) { 'https://try.gitea.io/api/v1' }
     let(:repo_root) { 'https://try.gitea.io' }
+    subject { described_class.new(project) }
+
     before do
       project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
     end
@@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
       let(:expected_not_called) { [:import_releases] }
     end
     it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+    it_behaves_like 'Gitlab::GithubImport unit-testing'
 
     describe '#client' do
       it 'instantiates a Client' do
@@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
           { host: "#{repo_root}:443/foo", api_version: 'v1' }
         )
 
-        described_class.new(project).client
+        subject.client
       end
     end
   end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index e46be18aa99984aad6955af7e4e1512fbb0b54d6..44423917944ccbce212e6a37fe50e198413e6f8d 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -7,10 +7,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
   let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
   let(:repository) { double(id: 1, fork: false) }
   let(:source_repo) { repository }
-  let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
+  let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
+  let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
   let(:target_repo) { repository }
   let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
   let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
+  let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
   let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
   let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
   let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -49,7 +51,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
           title: 'New feature',
           description: "*Created by: octocat*\n\nPlease pull these awesome changes",
           source_project: project,
-          source_branch: 'feature',
+          source_branch: 'branch-merged',
           source_branch_sha: source_sha,
           target_project: project,
           target_branch: 'master',
@@ -75,7 +77,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
           title: 'New feature',
           description: "*Created by: octocat*\n\nPlease pull these awesome changes",
           source_project: project,
-          source_branch: 'feature',
+          source_branch: 'branch-merged',
           source_branch_sha: source_sha,
           target_project: project,
           target_branch: 'master',
@@ -102,7 +104,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
           title: 'New feature',
           description: "*Created by: octocat*\n\nPlease pull these awesome changes",
           source_project: project,
-          source_branch: 'feature',
+          source_branch: 'branch-merged',
           source_branch_sha: source_sha,
           target_project: project,
           target_branch: 'master',
@@ -194,7 +196,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       let(:raw_data) { double(base_data) }
 
       it 'returns branch ref' do
-        expect(pull_request.source_branch_name).to eq 'feature'
+        expect(pull_request.source_branch_name).to eq 'branch-merged'
       end
     end
 
@@ -205,10 +207,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
         expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
       end
     end
+
+    context 'when source branch is from a fork' do
+      let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+      it 'prefixes branch name with pull request number and project with namespace to avoid collision' do
+        expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master'
+      end
+    end
   end
 
   shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
-    context 'when source branch exists' do
+    context 'when target branch exists' do
       let(:raw_data) { double(base_data) }
 
       it 'returns branch ref' do
@@ -271,6 +281,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
     end
   end
 
+  describe '#cross_project?' do
+    context 'when source and target repositories are different' do
+      let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+      it 'returns true' do
+        expect(pull_request.cross_project?).to eq true
+      end
+    end
+
+    context 'when source and target repositories are the same' do
+      let(:raw_data) { double(base_data.merge(head: source_branch)) }
+
+      it 'returns false' do
+        expect(pull_request.cross_project?).to eq false
+      end
+    end
+  end
+
   describe '#url' do
     let(:raw_data) { double(base_data) }
 
@@ -278,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
     end
   end
+
+  describe '#opened?' do
+    let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+    it 'returns true when state is "open"' do
+      expect(pull_request.opened?).to be_truthy
+    end
+  end
 end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e177d8831587cf7150863c8db97c986b691f6911..e49799ad10502bcd8d926db39d7c6f7cf63bdd88 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
     end
 
     it 'highlights all the lines properly' do
-      expect(lines[4]).to eq(%Q{<span id="LC5" class="line">  <span class="kp">extend</span> <span class="nb">self</span></span>\n})
-      expect(lines[21]).to eq(%Q{<span id="LC22" class="line">    <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
-      expect(lines[26]).to eq(%Q{<span id="LC27" class="line">    <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
+      expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby">  <span class="kp">extend</span> <span class="nb">self</span></span>\n})
+      expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby">    <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
+      expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby">    <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
     end
 
     describe 'with CRLF' do
@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
       end
 
       it 'strips extra LFs' do
-        expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test  </span>")
+        expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test  </span>")
       end
     end
   end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9c08f41fe82d6e412dff44f0dc4c88a25d3bd57e..c3ee743035a4c5d64ff2c102d299b39e66cbe3ce 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -97,6 +97,7 @@ variables:
 triggers:
 - project
 - trigger_requests
+- owner
 deploy_keys:
 - user
 - deploy_keys_projects
@@ -131,12 +132,12 @@ project:
 - campfire_service
 - drone_ci_service
 - emails_on_push_service
-- builds_email_service
 - pipelines_email_service
 - mattermost_slash_commands_service
 - slack_slash_commands_service
 - irker_service
 - pivotaltracker_service
+- prometheus_service
 - hipchat_service
 - flowdock_service
 - assembla_service
@@ -155,6 +156,7 @@ project:
 - gitlab_issue_tracker_service
 - external_wiki_service
 - kubernetes_service
+- mock_ci_service
 - forked_project_link
 - forked_from_project
 - forked_project_links
@@ -200,6 +202,7 @@ project:
 - route
 - statistics
 - container_images
+- uploads
 award_emoji:
 - awardable
 - user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index ea65a5dfed11a4c8e4446f5499717255a19d1c0d..e24d070706a746050dd66bd01706a3d9491a28aa 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -17,7 +17,7 @@ describe 'Import/Export attribute configuration', lib: true do
     # Remove duplicated or add missing models
     # - project is not part of the tree, so it has to be added manually.
     # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
-    names.flatten.uniq - ['milestones', 'labels'] + ['project']
+    names.flatten.uniq - %w(milestones labels) + ['project']
   end
 
   let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
index d6ee94442cbc7a6ef3a3fc5f6091fee15a886ddd..579a31ead58b035622498c1294e79c71d20ab91c 100644
--- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
 
 describe Gitlab::ImportExport::AvatarSaver, lib: true do
   let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
-  let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+  let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
   let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
   let(:project) { create(:empty_project) }
 
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
index a88ddd17aca69356ca3745466c8806d5a45825b9..b88b9c18c158ac1eb42d405415c657d30d0d8903 100644
--- a/spec/lib/gitlab/import_export/file_importer_spec.rb
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
 
 describe Gitlab::ImportExport::FileImporter, lib: true do
   let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
-  let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" }
+  let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" }
   let(:valid_file) { "#{shared.export_path}/valid.json" }
   let(:symlink_file) { "#{shared.export_path}/invalid.json" }
   let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
index 20743811dab9b302c7cdd221b65b5c70abb6fdc8..f3fd0d8287592490a4f7f10603e13bac7d72961a 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::ImportExport, services: true do
     end
 
     it 'contains the namespace path' do
-      expect(described_class.export_filename(project: project)).to include(project.namespace.full_path)
+      expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_'))
     end
 
     it 'does not go over a certain length' do
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 9b492d1b9c7b4a0e24c73ed84639b5d151588154..2ede5cdd2adbf7bd5fb415cdaa51f8526a71bba5 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -14,7 +14,7 @@ describe 'Import/Export model configuration', lib: true do
     # - project is not part of the tree, so it has to be added manually.
     # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
     # - User, Author... Models we do not care about for checking models
-    names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+    names.flatten.uniq - %w(milestones labels user author) + ['project']
   end
 
   let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 2e9f60432b4577b156a9bd94d2d71a4e0798a440..d9b674268180ed1052e316cc87e38c2d8053fa22 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2539,7 +2539,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": true,
+      "merge_when_pipeline_succeeds": true,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -2976,7 +2976,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -3260,7 +3260,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -3544,7 +3544,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -4234,7 +4234,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -4782,7 +4782,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -5281,7 +5281,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -5541,7 +5541,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -6231,7 +6231,7 @@
       "merge_params": {
         "force_remove_source_branch": null
       },
-      "merge_when_build_succeeds": false,
+      "merge_when_pipeline_succeeds": false,
       "merge_user_id": null,
       "merge_commit_sha": null,
       "deleted_at": null,
@@ -6507,7 +6507,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6565,7 +6564,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6603,7 +6601,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
           },
@@ -6624,7 +6621,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6659,7 +6655,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
           },
@@ -6695,7 +6690,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6716,7 +6710,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6751,7 +6744,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
           },
@@ -6787,7 +6779,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
           },
@@ -6808,7 +6799,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6843,7 +6833,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
           },
@@ -6879,7 +6868,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
           },
@@ -6900,7 +6888,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6935,7 +6922,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6971,7 +6957,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
           },
@@ -6985,11 +6970,10 @@
     {
       "id": 123,
       "token": "cdbfasdf44a5958c83654733449e585",
-      "project_id": null,
+      "project_id": 5,
       "deleted_at": null,
       "created_at": "2017-01-16T15:25:28.637Z",
-      "updated_at": "2017-01-16T15:25:28.637Z",
-      "gl_project_id": 123
+      "updated_at": "2017-01-16T15:25:28.637Z"
     }
   ],
   "deploy_keys": [
@@ -7047,7 +7031,7 @@
       "updated_at": "2016-06-14T15:01:51.303Z",
       "active": false,
       "properties": {
-        "notify_only_broken_builds": true
+        "notify_only_broken_pipelines": true
       },
       "template": false,
       "push_events": true,
@@ -7055,7 +7039,7 @@
       "merge_requests_events": true,
       "tag_push_events": true,
       "note_events": true,
-      "build_events": true,
+      "pipeline_events": true,
       "category": "common",
       "default": false,
       "wiki_page_events": true
@@ -7174,7 +7158,7 @@
       "updated_at": "2016-06-14T15:01:51.219Z",
       "active": false,
       "properties": {
-        "notify_only_broken_builds": true
+        "notify_only_broken_pipelines": true
       },
       "template": false,
       "push_events": true,
@@ -7182,7 +7166,7 @@
       "merge_requests_events": true,
       "tag_push_events": true,
       "note_events": true,
-      "build_events": true,
+      "pipeline_events": true,
       "category": "common",
       "default": false,
       "wiki_page_events": true
@@ -7334,27 +7318,6 @@
       "default": false,
       "wiki_page_events": true
     },
-    {
-      "id": 85,
-      "title": "Builds emails",
-      "project_id": 5,
-      "created_at": "2016-06-14T15:01:51.090Z",
-      "updated_at": "2016-06-14T15:01:51.090Z",
-      "active": false,
-      "properties": {
-        "notify_only_broken_builds": true
-      },
-      "template": false,
-      "push_events": true,
-      "issues_events": true,
-      "merge_requests_events": true,
-      "tag_push_events": true,
-      "note_events": true,
-      "build_events": true,
-      "category": "common",
-      "default": false,
-      "wiki_page_events": true
-    },
     {
       "id": 84,
       "title": "Buildkite",
@@ -7503,4 +7466,4 @@
     "updated_at": "2016-09-23T11:58:28.000Z",
     "wiki_access_level": 20
   }
-}
\ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index f4a21c24fa1c1ecb245e5629baaf0311004e0ebd..c36f12dbd82237d94aa5687a9f236f62b32011cf 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
           expect(Ci::Build.where(token: 'abcd')).to be_empty
         end
       end
+
+      context 'has restored the correct number of records' do
+        it 'has the correct number of merge requests' do
+          expect(@project.merge_requests.size).to eq(9)
+        end
+
+        it 'has the correct number of triggers' do
+          expect(@project.triggers.size).to eq(1)
+        end
+
+        it 'has the correct number of pipelines and statuses' do
+          expect(@project.pipelines.size).to eq(5)
+
+          @project.pipelines.zip([2, 2, 2, 2, 2])
+            .each do |(pipeline, expected_status_size)|
+              expect(pipeline.statuses.size).to eq(expected_status_size)
+            end
+        end
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 3628adefc0cf66fcb0c781b36336515a50b4d44a..012c22ec5ad2ad6be93b1ce88abe29475f041d73 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
   describe 'saves the project tree into a json object' do
     let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
     let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
-    let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+    let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
     let(:user) { create(:user) }
     let(:project) { setup_project }
 
@@ -114,7 +114,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
       it 'has project and group labels' do
         label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] }
 
-        expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
+        expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
       end
 
       it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
index d39ea60ff7f779e11568bbfdb73ad4c43ec4e760..a7f4e11271e38549939b3616284df1d445fd8858 100644
--- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::RepoSaver, services: true do
   describe 'bundle a project Git repo' do
     let(:user) { create(:user) }
     let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
-    let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+    let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
     let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
     let(:bundler) { described_class.new(project: project, shared: shared) }
 
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index c5ac702d83100ec942b036004f0d2e1faf9127f8..1ad16a9b57dd2fb5f68b869835b3eb25a3e703a4 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,12 +15,14 @@ Issue:
 - updated_by_id
 - confidential
 - deleted_at
+- closed_at
 - due_date
 - moved_to_id
 - lock_version
 - milestone_id
 - weight
 - time_estimate
+- relative_position
 Event:
 - id
 - target_type
@@ -142,7 +144,7 @@ MergeRequest:
 - updated_by_id
 - merge_error
 - merge_params
-- merge_when_build_succeeds
+- merge_when_pipeline_succeeds
 - merge_user_id
 - merge_commit_sha
 - deleted_at
@@ -175,7 +177,6 @@ Ci::Pipeline:
 - tag
 - yaml_errors
 - committed_at
-- gl_project_id
 - status
 - started_at
 - finished_at
@@ -210,7 +211,6 @@ CommitStatus:
 - target_url
 - description
 - artifacts_file
-- gl_project_id
 - artifacts_metadata
 - erased_by_id
 - erased_at
@@ -231,7 +231,6 @@ Ci::Variable:
 - encrypted_value
 - encrypted_value_salt
 - encrypted_value_iv
-- gl_project_id
 Ci::Trigger:
 - id
 - token
@@ -239,7 +238,8 @@ Ci::Trigger:
 - deleted_at
 - created_at
 - updated_at
-- gl_project_id
+- owner_id
+- description
 DeployKey:
 - id
 - user_id
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
index 47d5d2fc150ed3f0d9f9bcdf6a31c1ec0bb191ea..071e5fac3f0d6a2cb991e6ad1b8f21421b931f9a 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::WikiRepoSaver, services: true do
   describe 'bundle a wiki Git repo' do
     let(:user) { create(:user) }
     let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
-    let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+    let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
     let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
     let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
     let!(:project_wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 8cea38e9ff85fd40f610a1b668c323ae1116cdfd..b3b5e5e7e33d5217bb4181c16f0bf6ebfcc8e2e6 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -22,16 +22,16 @@ describe Gitlab::ImportSources do
   describe '.values' do
     it 'returns an array' do
       expected =
-        [
-          'github',
-          'bitbucket',
-          'gitlab',
-          'google_code',
-          'fogbugz',
-          'git',
-          'gitlab_project',
-          'gitea'
-        ]
+        %w(
+          github
+          bitbucket
+          gitlab
+          google_code
+          fogbugz
+          git
+          gitlab_project
+          gitea
+        )
 
       expect(described_class.values).to eq(expected)
     end
@@ -40,15 +40,15 @@ describe Gitlab::ImportSources do
   describe '.importer_names' do
     it 'returns an array of importer names' do
       expected =
-        [
-          'github',
-          'bitbucket',
-          'gitlab',
-          'google_code',
-          'fogbugz',
-          'gitlab_project',
-          'gitea'
-        ]
+        %w(
+          github
+          bitbucket
+          gitlab
+          google_code
+          fogbugz
+          gitlab_project
+          gitea
+        )
 
       expect(described_class.importer_names).to eq(expected)
     end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index c9bd52a3b8fa85acac312ba5673a6fb9b701b526..91f9d06b85ae4c11f96b4b37aa0a01f4df5902df 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Kubernetes do
     let(:pod_name) { 'pod1' }
     let(:container_name) { 'container1' }
 
-    subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
+    subject(:result) { URI.parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
 
     it { expect(result.scheme).to eq('wss') }
     it { expect(result.host).to eq('example.com') }
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 69c49051156681ea80bb52c2e299706f68a4a696..7a2f774b9481cd9927d00b72b566e0a70e0aa64a 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::LDAP::AuthHash, lib: true do
   context "with overridden attributes" do
     let(:attributes) do
       {
-        'username'  => ['mail', 'email'],
+        'username'  => %w(mail email),
         'name'      => 'fullName'
       }
     end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 89790c9e1af45db0eab05055be93375fd33c11a8..2f3bd4393b7601932acc2692d81a996ab6e7f900 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -95,10 +95,10 @@ describe Gitlab::LDAP::User, lib: true do
 
     it 'maintains an identity per provider' do
       existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
-      expect(existing_user.identities.count).to eql(1)
+      expect(existing_user.identities.count).to be(1)
 
       ldap_user.save
-      expect(ldap_user.gl_user.identities.count).to eql(2)
+      expect(ldap_user.gl_user.identities.count).to be(2)
 
       # Expect that find_by provider only returns a single instance of an identity and not an Enumerable
       expect(ldap_user.gl_user.identities.find_by(provider: 'twitter')).to be_instance_of Identity
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index d88bcae41fb854e6eb278c630b3944f3631064aa..a986cb520fb0c30d032caaa8c5cb13412d8aaca0 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -197,11 +197,13 @@ describe Gitlab::Metrics::Instrumentation do
 
       @child1 = Class.new(@dummy) do
         def self.child1_foo; end
+
         def child1_bar; end
       end
 
       @child2 = Class.new(@child1) do
         def self.child2_foo; end
+
         def child2_bar; end
       end
     end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 8d05081eecbce700006d2265264f1bb574b0a21d..a247f03b2da917a68fb665e1d7339e4a87afc20a 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Metrics::MethodCall do
 
       expect(metric.values[:duration]).to be_a_kind_of(Numeric)
       expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
-      expect(metric.values[:call_count]).to an_instance_of(Fixnum)
+      expect(metric.values[:call_count]).to be_an(Integer)
 
       expect(metric.tags).to eq({ method: 'Foo#bar' })
     end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f26fca52c5067a77d0d7679fc6fabc802ee462a0..d240b8a01fd7c10af201a990f89b26d2b5e0ecb3 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Metrics::Metric do
       end
 
       it 'includes the timestamp' do
-        expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
+        expect(hash[:timestamp]).to be_an(Integer)
       end
     end
   end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 9e2ea89a712a0733b77b6e39a266efcad695637f..4d94d8705fbe749301d13b299a6e1ef50cbe50dd 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -29,19 +29,19 @@ describe Gitlab::Metrics::System do
 
   describe '.cpu_time' do
     it 'returns a Fixnum' do
-      expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
+      expect(described_class.cpu_time).to be_an(Integer)
     end
   end
 
   describe '.real_time' do
     it 'returns a Fixnum' do
-      expect(described_class.real_time).to be_an_instance_of(Fixnum)
+      expect(described_class.real_time).to be_an(Integer)
     end
   end
 
   describe '.monotonic_time' do
     it 'returns a Fixnum' do
-      expect(described_class.monotonic_time).to be_an_instance_of(Fixnum)
+      expect(described_class.monotonic_time).to be_an(Integer)
     end
   end
 end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 3887c04c83214b19f531523afd6e646e0c3104f1..0c5a6246d85156eed8714fc9955f5c662bdf08a7 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -134,7 +134,7 @@ describe Gitlab::Metrics::Transaction do
         series:    'rails_transactions',
         tags:      { action: 'Foo#bar' },
         values:    { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
-        timestamp: an_instance_of(Fixnum)
+        timestamp: a_kind_of(Integer)
       }
 
       expect(Gitlab::Metrics).to receive(:submit_metrics).
@@ -151,7 +151,7 @@ describe Gitlab::Metrics::Transaction do
         series:    'events',
         tags:      { event: :meow },
         values:    { count: 1 },
-        timestamp: an_instance_of(Fixnum)
+        timestamp: a_kind_of(Integer)
       }
 
       expect(Gitlab::Metrics).to receive(:submit_metrics).
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index fd3769d75b5162896c884133ffdc214dcb3e9614..c2ab015d5cb23c58ad14b3eed415463e8c27197f 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -15,16 +15,93 @@ describe Gitlab::Middleware::Go, lib: true do
     end
 
     describe 'when go-get=1' do
-      it 'returns a document' do
-        env = { 'rack.input' => '',
-                'QUERY_STRING' => 'go-get=1',
-                'PATH_INFO' => '/group/project/path' }
-        resp = middleware.call(env)
-        expect(resp[0]).to eq(200)
-        expect(resp[1]['Content-Type']).to eq('text/html')
-        expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n"
-        expect(resp[2].body).to eq([expected_body])
+      let(:current_user) { nil }
+
+      context 'with simple 2-segment project path' do
+        let!(:project) { create(:project, :private) }
+
+        context 'with subpackages' do
+          let(:path) { "#{project.full_path}/subpackage" }
+
+          it 'returns the full project path' do
+            expect_response_with_path(go, project.full_path)
+          end
+        end
+
+        context 'without subpackages' do
+          let(:path) { project.full_path }
+
+          it 'returns the full project path' do
+            expect_response_with_path(go, project.full_path)
+          end
+        end
+      end
+
+      context 'with a nested project path' do
+        let(:group) { create(:group, :nested) }
+        let!(:project) { create(:project, :public, namespace: group) }
+
+        shared_examples 'a nested project' do
+          context 'when the project is public' do
+            it 'returns the full project path' do
+              expect_response_with_path(go, project.full_path)
+            end
+          end
+
+          context 'when the project is private' do
+            before do
+              project.update_attribute(:visibility_level, Project::PRIVATE)
+            end
+
+            context 'with access to the project' do
+              let(:current_user) { project.creator }
+
+              before do
+                project.team.add_master(current_user)
+              end
+
+              it 'returns the full project path' do
+                expect_response_with_path(go, project.full_path)
+              end
+            end
+
+            context 'without access to the project' do
+              it 'returns the 2-segment group path' do
+                expect_response_with_path(go, group.full_path)
+              end
+            end
+          end
+        end
+
+        context 'with subpackages' do
+          let(:path) { "#{project.full_path}/subpackage" }
+
+          it_behaves_like 'a nested project'
+        end
+
+        context 'without subpackages' do
+          let(:path) { project.full_path }
+
+          it_behaves_like 'a nested project'
+        end
       end
     end
+
+    def go
+      env = {
+        'rack.input' => '',
+        'QUERY_STRING' => 'go-get=1',
+        'PATH_INFO' => "/#{path}",
+        'warden' => double(authenticate: current_user)
+      }
+      middleware.call(env)
+    end
+
+    def expect_response_with_path(response, path)
+      expect(response[0]).to eq(200)
+      expect(response[1]['Content-Type']).to eq('text/html')
+      expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n"
+      expect(response[2].body).to eq([expected_body])
+    end
   end
 end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index fc9e1cb430a0e05019618010b26c51de57b506cd..6c84a4c8b7368f0fa68f8088679c4db0c8ed29a9 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -148,12 +148,14 @@ describe Gitlab::OAuth::User, lib: true do
                 expect(gl_user).to be_valid
                 expect(gl_user.username).to eql uid
                 expect(gl_user.email).to eql 'johndoe@example.com'
-                expect(gl_user.identities.length).to eql 2
+                expect(gl_user.identities.length).to be 2
                 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
                 expect(identities_as_hash).to match_array(
-                  [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+                  [
+                    { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
                     { provider: 'twitter', extern_uid: uid }
-                  ])
+                  ]
+                )
               end
             end
 
@@ -167,12 +169,14 @@ describe Gitlab::OAuth::User, lib: true do
                 expect(gl_user).to be_valid
                 expect(gl_user.username).to eql 'john'
                 expect(gl_user.email).to eql 'john@example.com'
-                expect(gl_user.identities.length).to eql 2
+                expect(gl_user.identities.length).to be 2
                 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
                 expect(identities_as_hash).to match_array(
-                  [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+                  [
+                    { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
                     { provider: 'twitter', extern_uid: uid }
-                  ])
+                  ]
+                )
               end
             end
 
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
index 498dc514c8c2b45caf276bd952ddcc846faebe17..acce2be93f2dd812cfd7bb38897f748d163ec83a 100644
--- a/spec/lib/gitlab/optimistic_locking_spec.rb
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -1,10 +1,10 @@
 require 'spec_helper'
 
 describe Gitlab::OptimisticLocking, lib: true do
-  describe '#retry_lock' do
-    let!(:pipeline) { create(:ci_pipeline) }
-    let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+  let!(:pipeline) { create(:ci_pipeline) }
+  let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
 
+  describe '#retry_lock' do
     it 'does not reload object if state changes' do
       expect(pipeline).not_to receive(:reload)
       expect(pipeline).to receive(:succeed).and_call_original
@@ -36,4 +36,17 @@ describe Gitlab::OptimisticLocking, lib: true do
       end.to raise_error(ActiveRecord::StaleObjectError)
     end
   end
+
+  describe '#retry_optimistic_lock' do
+    context 'when locking module is mixed in' do
+      let(:unlockable) do
+        Class.new.include(described_class).new
+      end
+
+      it 'is an alias for retry_lock' do
+        expect(unlockable.method(:retry_optimistic_lock))
+          .to eq unlockable.method(:retry_lock)
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..280264188e2e29b074c58f723bf839675b2f7cd8
--- /dev/null
+++ b/spec/lib/gitlab/prometheus_spec.rb
@@ -0,0 +1,143 @@
+require 'spec_helper'
+
+describe Gitlab::Prometheus, lib: true do
+  include PrometheusHelpers
+
+  subject { described_class.new(api_url: 'https://prometheus.example.com') }
+
+  describe '#ping' do
+    it 'issues a "query" request to the API endpoint' do
+      req_stub = stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector'))
+
+      expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] })
+      expect(req_stub).to have_been_requested
+    end
+  end
+
+  # This shared examples expect:
+  # - query_url: A query URL
+  # - execute_query: A query call
+  shared_examples 'failure response' do
+    context 'when request returns 400 with an error message' do
+      it 'raises a Gitlab::PrometheusError error' do
+        req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' })
+
+        expect { execute_query }
+          .to raise_error(Gitlab::PrometheusError, 'bar!')
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns 400 without an error message' do
+      it 'raises a Gitlab::PrometheusError error' do
+        req_stub = stub_prometheus_request(query_url, status: 400)
+
+        expect { execute_query }
+          .to raise_error(Gitlab::PrometheusError, 'Bad data received')
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns 500' do
+      it 'raises a Gitlab::PrometheusError error' do
+        req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' })
+
+        expect { execute_query }
+          .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}')
+        expect(req_stub).to have_been_requested
+      end
+    end
+  end
+
+  describe '#query' do
+    let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+    let(:query_url) { prometheus_query_url(prometheus_query) }
+
+    context 'when request returns vector results' do
+      it 'returns data from the API call' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+        expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns matrix results' do
+      it 'returns nil' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('matrix'))
+
+        expect(subject.query(prometheus_query)).to be_nil
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns no data' do
+      it 'returns []' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('vector'))
+
+        expect(subject.query(prometheus_query)).to be_empty
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    it_behaves_like 'failure response' do
+      let(:execute_query) { subject.query(prometheus_query) }
+    end
+  end
+
+  describe '#query_range' do
+    let(:prometheus_query) { prometheus_memory_query('env-slug') }
+    let(:query_url) { prometheus_query_range_url(prometheus_query) }
+
+    around do |example|
+      Timecop.freeze { example.run }
+    end
+
+    context 'when a start time is passed' do
+      let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) }
+
+      it 'passed it in the requested URL' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+        subject.query_range(prometheus_query, start: 2.hours.ago)
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns vector results' do
+      it 'returns nil' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+        expect(subject.query_range(prometheus_query)).to be_nil
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns matrix results' do
+      it 'returns data from the API call' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('matrix'))
+
+        expect(subject.query_range(prometheus_query)).to eq([
+          {
+            "metric" => {},
+            "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]]
+          }
+        ])
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'when request returns no data' do
+      it 'returns []' do
+        req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('matrix'))
+
+        expect(subject.query_range(prometheus_query)).to be_empty
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    it_behaves_like 'failure response' do
+      let(:execute_query) { subject.query_range(prometheus_query) }
+    end
+  end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index 917c5c46db11e864e542c832d18ee8ca108fb059..8b77c925705b36f9728ac577894792b9f019404b 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,8 +3,16 @@ require 'spec_helper'
 describe Gitlab::Redis do
   include StubENV
 
-  before(:each) { clear_raw_config }
-  after(:each) { clear_raw_config }
+  let(:config) { 'config/resque.yml' }
+
+  before(:each) do
+    stub_env('GITLAB_REDIS_CONFIG_FILE', Rails.root.join(config).to_s)
+    clear_raw_config
+  end
+
+  after(:each) do
+    clear_raw_config
+  end
 
   describe '.params' do
     subject { described_class.params }
@@ -18,22 +26,22 @@ describe Gitlab::Redis do
     end
 
     context 'when url contains unix socket reference' do
-      let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
-      let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
+      let(:config_old) { 'spec/fixtures/config/redis_old_format_socket.yml' }
+      let(:config_new) { 'spec/fixtures/config/redis_new_format_socket.yml' }
 
       context 'with old format' do
-        it 'returns path key instead' do
-          stub_const("#{described_class}::CONFIG_FILE", config_old)
+        let(:config) { config_old }
 
+        it 'returns path key instead' do
           is_expected.to include(path: '/path/to/old/redis.sock')
           is_expected.not_to have_key(:url)
         end
       end
 
       context 'with new format' do
-        it 'returns path key instead' do
-          stub_const("#{described_class}::CONFIG_FILE", config_new)
+        let(:config) { config_new }
 
+        it 'returns path key instead' do
           is_expected.to include(path: '/path/to/redis.sock')
           is_expected.not_to have_key(:url)
         end
@@ -41,22 +49,22 @@ describe Gitlab::Redis do
     end
 
     context 'when url is host based' do
-      let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
-      let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+      let(:config_old) { 'spec/fixtures/config/redis_old_format_host.yml' }
+      let(:config_new) { 'spec/fixtures/config/redis_new_format_host.yml' }
 
       context 'with old format' do
-        it 'returns hash with host, port, db, and password' do
-          stub_const("#{described_class}::CONFIG_FILE", config_old)
+        let(:config) { config_old }
 
+        it 'returns hash with host, port, db, and password' do
           is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
           is_expected.not_to have_key(:url)
         end
       end
 
       context 'with new format' do
-        it 'returns hash with host, port, db, and password' do
-          stub_const("#{described_class}::CONFIG_FILE", config_new)
+        let(:config) { config_new }
 
+        it 'returns hash with host, port, db, and password' do
           is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
           is_expected.not_to have_key(:url)
         end
@@ -74,15 +82,13 @@ describe Gitlab::Redis do
     end
 
     context 'when yml file with env variable' do
-      let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') }
+      let(:config) { 'spec/fixtures/config/redis_config_with_env.yml' }
 
       before  do
         stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379')
       end
 
       it 'reads redis url from env variable' do
-        stub_const("#{described_class}::CONFIG_FILE", redis_config)
-
         expect(described_class.url).to eq 'redis://redishost:6379'
       end
     end
@@ -90,14 +96,13 @@ describe Gitlab::Redis do
 
   describe '._raw_config' do
     subject { described_class._raw_config }
+    let(:config) { '/var/empty/doesnotexist' }
 
     it 'should be frozen' do
       expect(subject).to be_frozen
     end
 
     it 'returns false when the file does not exist' do
-      stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
-
       expect(subject).to eq(false)
     end
   end
@@ -134,22 +139,18 @@ describe Gitlab::Redis do
     subject { described_class.new(Rails.env).sentinels }
 
     context 'when sentinels are defined' do
-      let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+      let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
 
       it 'returns an array of hashes with host and port keys' do
-        stub_const("#{described_class}::CONFIG_FILE", config)
-
         is_expected.to include(host: 'localhost', port: 26380)
         is_expected.to include(host: 'slave2', port: 26381)
       end
     end
 
     context 'when sentinels are not defined' do
-      let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+      let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
 
       it 'returns nil' do
-        stub_const("#{described_class}::CONFIG_FILE", config)
-
         is_expected.to be_nil
       end
     end
@@ -159,21 +160,17 @@ describe Gitlab::Redis do
     subject { described_class.new(Rails.env).sentinels? }
 
     context 'when sentinels are defined' do
-      let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+      let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
 
       it 'returns true' do
-        stub_const("#{described_class}::CONFIG_FILE", config)
-
         is_expected.to be_truthy
       end
     end
 
     context 'when sentinels are not defined' do
-      let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+      let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
 
       it 'returns false' do
-        stub_const("#{described_class}::CONFIG_FILE", config)
-
         is_expected.to be_falsey
       end
     end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 089ec4e2737a8d797833c2638ca91b60ebb2f811..ba45e2d758ceac93e1858fdf21990ead5a17e4f4 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -51,8 +51,8 @@ describe Gitlab::Regex, lib: true do
     it { is_expected.not_to match('foo-') }
   end
 
-  describe 'NAMESPACE_REF_REGEX_STR' do
-    subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} }
+  describe 'FULL_NAMESPACE_REGEX_STR' do
+    subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
 
     it { is_expected.to match('gitlab.org') }
     it { is_expected.to match('gitlab.org/gitlab-git') }
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a91c8655cddbc0f21864056b3355727bdeb2086b
--- /dev/null
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::RequestContext, lib: true do
+  describe '#client_ip' do
+    subject { Gitlab::RequestContext.client_ip }
+    let(:app) { -> (env) {} }
+    let(:env) { Hash.new }
+
+    context 'when RequestStore::Middleware is used' do
+      around(:each) do |example|
+        RequestStore::Middleware.new(-> (env) { example.run }).call({})
+      end
+
+      context 'request' do
+        let(:ip) { '192.168.1.11' }
+
+        before do
+          allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+          Gitlab::RequestContext.new(app).call(env)
+        end
+
+        it { is_expected.to eq(ip) }
+      end
+
+      context 'before RequestContext middleware run' do
+        it { is_expected.to be_nil }
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 02c139f1a0d131e9f4eeae8cd0850b0dfe1dd732..4f6ef3c10fc6ba15935ce5f166b99efd1bd62838 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -155,11 +155,10 @@ describe Gitlab::Saml::User, lib: true do
                 expect(gl_user).to be_valid
                 expect(gl_user.username).to eql uid
                 expect(gl_user.email).to eql 'john@mail.com'
-                expect(gl_user.identities.length).to eql 2
+                expect(gl_user.identities.length).to be 2
                 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
-                expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
-                                                            { provider: 'saml', extern_uid: uid }
-                                                          ])
+                expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+                                                           { provider: 'saml', extern_uid: uid }])
               end
             end
 
@@ -178,11 +177,10 @@ describe Gitlab::Saml::User, lib: true do
                 expect(gl_user).to be_valid
                 expect(gl_user.username).to eql 'john'
                 expect(gl_user.email).to eql 'john@mail.com'
-                expect(gl_user.identities.length).to eql 2
+                expect(gl_user.identities.length).to be 2
                 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
-                expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
-                                                            { provider: 'saml', extern_uid: uid }
-                                                          ])
+                expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+                                                           { provider: 'saml', extern_uid: uid }])
               end
 
               it 'saves successfully on subsequent tries, when both identities are present' do
@@ -204,11 +202,10 @@ describe Gitlab::Saml::User, lib: true do
                 local_gl_user = local_saml_user.gl_user
 
                 expect(local_gl_user).to be_valid
-                expect(local_gl_user.identities.length).to eql 2
+                expect(local_gl_user.identities.length).to be 2
                 identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
-                expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
-                                                            { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }
-                                                          ])
+                expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+                                                           { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }])
               end
             end
           end
diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
index b810c68ea035bd29172a044817f0f0b48927a6e9..c4b7fda5dbb422690fb3557537c61d28a4691fd9 100644
--- a/spec/lib/gitlab/serializer/ci/variables_spec.rb
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::Serializer::Ci::Variables do
   it 'converts keys into strings' do
     is_expected.to eq([
       { key: 'key', value: 'value', public: true },
-      { key: 'wee', value: 1, public: false }])
+      { key: 'wee', value: 1, public: false }
+    ])
   end
 end
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 0aa36a3416b7167dde0bf13b0cd5745643ea203c..56f06b61afbaf0813b6e54aa7cee3bef7bb1a79a 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -39,6 +39,32 @@ describe Gitlab::SidekiqStatus do
     end
   end
 
+  describe '.num_running', :redis do
+    it 'returns 0 if all jobs have been completed' do
+      expect(described_class.num_running(%w(123))).to eq(0)
+    end
+
+    it 'returns 2 if two jobs are still running' do
+      described_class.set('123')
+      described_class.set('456')
+
+      expect(described_class.num_running(%w(123 456 789))).to eq(2)
+    end
+  end
+
+  describe '.num_completed', :redis do
+    it 'returns 1 if all jobs have been completed' do
+      expect(described_class.num_completed(%w(123))).to eq(1)
+    end
+
+    it 'returns 1 if a job has not yet been completed' do
+      described_class.set('123')
+      described_class.set('456')
+
+      expect(described_class.num_completed(%w(123 456 789))).to eq(1)
+    end
+  end
+
   describe '.key_for' do
     it 'returns the key for a job ID' do
       key = described_class.key_for('123')
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 1335a2b8f3534261a63ae8c1edbb1c5a9ff0dc5a..9213ced7b194d342c93c2bb949e9f1dcf23882fa 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::IssueTemplate do
       create_template: {
         user: user,
         access: Gitlab::Access::MASTER,
-        path: 'issue_templates' })
+        path: 'issue_templates'
+      })
   end
 
   describe '.all' do
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index 320b870309a1ce33c882082c997a6c33c43dcfca..77dd3079e2254becb6d7fb6eafc6cd48b2e2af1d 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::MergeRequestTemplate do
       create_template: {
         user: user,
         access: Gitlab::Access::MASTER,
-        path: 'merge_request_templates' })
+        path: 'merge_request_templates'
+      })
   end
 
   describe '.all' do
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index edadab043d7bdf6189459ccc86f60f7027300669..fcfd8d58b707ec10b93a68c5c3018b3b3ddaa529 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -32,7 +32,8 @@ describe Gitlab::Upgrader, lib: true do
         '43af3e65a486a9237f29f56d96c3b3da59c24ae0  refs/tags/v7.11.2',
         'dac18e7728013a77410e926a1e64225703754a2d  refs/tags/v7.11.2^{}',
         '0bf21fd4b46c980c26fd8c90a14b86a4d90cc950  refs/tags/v7.9.4',
-        'b10de29edbaff7219547dc506cb1468ee35065c3  refs/tags/v7.9.4^{}'])
+        'b10de29edbaff7219547dc506cb1468ee35065c3  refs/tags/v7.9.4^{}'
+      ])
       expect(upgrader.latest_version_raw).to eq("v7.11.2")
     end
   end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a504d299307d74d4173b488d99f64fb6da0057d9
--- /dev/null
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBlocker, lib: true do
+  describe '#blocked_url?' do
+    it 'allows imports from configured web host and port' do
+      import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
+      expect(described_class.blocked_url?(import_url)).to be false
+    end
+
+    it 'allows imports from configured SSH host and port' do
+      import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
+      expect(described_class.blocked_url?(import_url)).to be false
+    end
+
+    it 'returns true for bad localhost hostname' do
+      expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true
+    end
+
+    it 'returns true for bad port' do
+      expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+    end
+
+    it 'returns true for invalid URL' do
+      expect(described_class.blocked_url?('http://:8080')).to be true
+    end
+
+    it 'returns false for legitimate URL' do
+      expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+    end
+  end
+end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 2cb74629da861c380ef76a30625a6c7b0fd4fd74..fc144a2556a4566a20ecb0d65a30693a2b30a2b2 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do
   let(:url_sanitizer) do
     described_class.new("https://github.com/me/project.git", credentials: credentials)
   end
+  let(:user) { double(:user, username: 'john.doe') }
 
   describe '.sanitize' do
     def sanitize_url(url)
@@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do
     end
   end
 
+  describe '.valid?' do
+    it 'validates url strings' do
+      expect(described_class.valid?(nil)).to be(false)
+      expect(described_class.valid?('valid@project:url.git')).to be(true)
+      expect(described_class.valid?('123://invalid:url')).to be(false)
+    end
+  end
+
+  describe '.http_credentials_for_user' do
+    it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) }
+    it { expect(described_class.http_credentials_for_user('foo')).to eq({}) }
+  end
+
   describe '#sanitized_url' do
     it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
   end
 
   describe '#credentials' do
     it { expect(url_sanitizer.credentials).to eq(credentials) }
+
+    context 'when user is given to #initialize' do
+      let(:url_sanitizer) do
+        described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+      end
+
+      it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) }
+    end
   end
 
   describe '#full_url' do
@@ -69,5 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do
 
       expect(sanitizer.full_url).to eq('user@server:project.git')
     end
+
+    context 'when user is given to #initialize' do
+      let(:url_sanitizer) do
+        described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+      end
+
+      it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") }
+    end
   end
 end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index d5d87310874c0d145ae09a3e81762d39c3658526..56772409989b064a7e6384410ce8443665a62cd7 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,5 @@
 describe Gitlab::Utils, lib: true do
-  def to_boolean(value)
-    described_class.to_boolean(value)
-  end
+  delegate :to_boolean, to: :described_class
 
   describe '.to_boolean' do
     it 'accepts booleans' do
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3255c6f1ef73dc762d723a1a5e13c297db4d55f7
--- /dev/null
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::VisibilityLevel, lib: true do
+  describe '.level_value' do
+    it 'converts "public" to integer value' do
+      expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC)
+    end
+
+    it 'converts string integer to integer value' do
+      expect(described_class.level_value('20')).to eq(20)
+    end
+
+    it 'defaults to PRIVATE when string value is not valid' do
+      expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE)
+    end
+
+    it 'defaults to PRIVATE when integer value is not valid' do
+      expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index a32c6131030e619839d3e95436efcf055ea2acf7..8e5e8288c49b44a121e6f7ae503d7e60b9202599 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -199,4 +199,58 @@ describe Gitlab::Workhorse, lib: true do
       end
     end
   end
+
+  describe '.set_key_and_notify' do
+    let(:key) { 'test-key' }
+    let(:value) { 'test-value' }
+
+    subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) }
+
+    shared_examples 'set and notify' do
+      it 'set and return the same value' do
+        is_expected.to eq(value)
+      end
+
+      it 'set and notify' do
+        expect_any_instance_of(Redis).to receive(:publish)
+          .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value")
+
+        subject
+      end
+    end
+
+    context 'when we set a new key' do
+      let(:overwrite) { true }
+
+      it_behaves_like 'set and notify'
+    end
+
+    context 'when we set an existing key' do
+      let(:old_value) { 'existing-key' }
+
+      before do
+        described_class.set_key_and_notify(key, old_value, overwrite: true)
+      end
+
+      context 'and overwrite' do
+        let(:overwrite) { true }
+
+        it_behaves_like 'set and notify'
+      end
+
+      context 'and do not overwrite' do
+        let(:overwrite) { false }
+
+        it 'try to set but return the previous value' do
+          is_expected.to eq(old_value)
+        end
+
+        it 'does not notify' do
+          expect_any_instance_of(Redis).not_to receive(:publish)
+
+          subject
+        end
+      end
+    end
+  end
 end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 5ccf11008980a827f48a146fe31f22f92a87b9b1..4b5938edeb9a6f681083933eed36efdaf18e7895 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -13,8 +13,7 @@ describe Mattermost::Command do
   describe '#create' do
     let(:params) do
       { team_id: 'abc',
-        trigger: 'gitlab'
-      }
+        trigger: 'gitlab' }
     end
 
     subject { described_class.new(nil).create(params) }
@@ -24,7 +23,8 @@ describe Mattermost::Command do
         stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
           with(body: {
             team_id: 'abc',
-            trigger: 'gitlab' }.to_json).
+            trigger: 'gitlab'
+          }.to_json).
           to_return(
             status: 200,
             headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 2d14be6bcc2d277b017f9ff8ec48923633e33a71..ac493fdb20f7aa224bbb14f9cf5574f4afdebc56 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -13,19 +13,20 @@ describe Mattermost::Team do
 
     context 'for valid request' do
       let(:response) do
-        [{
-           "id" => "xiyro8huptfhdndadpz8r3wnbo",
-           "create_at" => 1482174222155,
-           "update_at" => 1482174222155,
-           "delete_at" => 0,
-           "display_name" => "chatops",
-           "name" => "chatops",
-           "email" => "admin@example.com",
-           "type" => "O",
-           "company_name" => "",
-           "allowed_domains" => "",
-           "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
-           "allow_open_invite" => false }]
+        { "xiyro8huptfhdndadpz8r3wnbo" => {
+          "id" => "xiyro8huptfhdndadpz8r3wnbo",
+          "create_at" => 1482174222155,
+          "update_at" => 1482174222155,
+          "delete_at" => 0,
+          "display_name" => "chatops",
+          "name" => "chatops",
+          "email" => "admin@example.com",
+          "type" => "O",
+          "company_name" => "",
+          "allowed_domains" => "",
+          "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+          "allow_open_invite" => false
+        } }
       end
 
       before do
@@ -38,7 +39,7 @@ describe Mattermost::Team do
       end
 
       it 'returns a token' do
-        is_expected.to eq(response)
+        is_expected.to eq(response.values)
       end
     end
 
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
deleted file mode 100644
index d968096783c4a08c0c1a81991b80e712af0e5cf1..0000000000000000000000000000000000000000
--- a/spec/mailers/emails/builds_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'spec_helper'
-require 'email_spec'
-
-describe Notify do
-  include EmailSpec::Matchers
-
-  include_context 'gitlab email notification'
-
-  describe 'build notification email' do
-    let(:build) { create(:ci_build) }
-    let(:project) { build.project }
-
-    shared_examples 'build email' do
-      it 'contains name of project' do
-        is_expected.to have_body_text build.project_name
-      end
-
-      it 'contains link to project' do
-        is_expected.to have_body_text namespace_project_path(project.namespace, project)
-      end
-    end
-
-    shared_examples 'an email with X-GitLab headers containing build details' do
-      it 'has X-GitLab-Build* headers' do
-        is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
-        is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
-      end
-    end
-
-    describe 'build success' do
-      subject { Notify.build_success_email(build.id, 'wow@example.com') }
-      before { build.success }
-
-      it_behaves_like 'build email'
-      it_behaves_like 'an email with X-GitLab headers containing build details'
-      it_behaves_like 'an email with X-GitLab headers containing project details'
-
-      it 'has header indicating build status' do
-        is_expected.to have_header 'X-GitLab-Build-Status', 'success'
-      end
-
-      it 'has the correct subject' do
-        is_expected.to have_subject /Build success for/
-      end
-    end
-
-    describe 'build fail' do
-      subject { Notify.build_fail_email(build.id, 'wow@example.com') }
-      before { build.drop }
-
-      it_behaves_like 'build email'
-      it_behaves_like 'an email with X-GitLab headers containing build details'
-      it_behaves_like 'an email with X-GitLab headers containing project details'
-
-      it 'has header indicating build status' do
-        is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
-      end
-
-      it 'has the correct subject' do
-        is_expected.to have_subject /Build failed for/
-      end
-    end
-  end
-end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index b692142713f98c098a3317ae797a8a2dcc220e18..6ee9157667693dcdcd0f9a803f6ef50635c0f53e 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -8,6 +8,15 @@ describe Notify do
 
   include_context 'gitlab email notification'
 
+  def have_referable_subject(referable, reply: false)
+    prefix = referable.project.name if referable.project
+    prefix = "Re: #{prefix}" if reply
+
+    suffix = "#{referable.title} (#{referable.to_reference})"
+
+    have_subject [prefix, suffix].compact.join(' | ')
+  end
+
   context 'for a project' do
     describe 'items that are assignable, the email' do
       let(:current_user) { create(:user, email: "current@email.com") }
@@ -41,11 +50,11 @@ describe Notify do
           it_behaves_like 'an unsubscribeable thread'
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
+            is_expected.to have_referable_subject(issue)
           end
 
           it 'contains a link to the new issue' do
-            is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+            is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
           end
 
           context 'when enabled email_author_in_body' do
@@ -54,8 +63,8 @@ describe Notify do
             end
 
             it 'contains a link to note author' do
-              is_expected.to have_body_text issue.author_name
-              is_expected.to have_body_text /wrote\:/
+              is_expected.to have_html_escaped_body_text issue.author_name
+              is_expected.to have_body_text 'wrote:'
             end
           end
         end
@@ -66,7 +75,7 @@ describe Notify do
           it_behaves_like 'it should show Gmail Actions View Issue link'
 
           it 'contains the description' do
-            is_expected.to have_body_text /#{issue_with_description.description}/
+            is_expected.to have_html_escaped_body_text issue_with_description.description
           end
         end
 
@@ -87,19 +96,19 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+            is_expected.to have_referable_subject(issue, reply: true)
           end
 
           it 'contains the name of the previous assignee' do
-            is_expected.to have_body_text /#{previous_assignee.name}/
+            is_expected.to have_html_escaped_body_text previous_assignee.name
           end
 
           it 'contains the name of the new assignee' do
-            is_expected.to have_body_text /#{assignee.name}/
+            is_expected.to have_html_escaped_body_text assignee.name
           end
 
           it 'contains a link to the issue' do
-            is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+            is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
           end
         end
 
@@ -121,15 +130,15 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+            is_expected.to have_referable_subject(issue, reply: true)
           end
 
           it 'contains the names of the added labels' do
-            is_expected.to have_body_text /foo, bar, and baz/
+            is_expected.to have_body_text 'foo, bar, and baz'
           end
 
           it 'contains a link to the issue' do
-            is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+            is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
           end
         end
 
@@ -150,19 +159,19 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i
+            is_expected.to have_referable_subject(issue, reply: true)
           end
 
           it 'contains the new status' do
-            is_expected.to have_body_text /#{status}/i
+            is_expected.to have_body_text status
           end
 
           it 'contains the user name' do
-            is_expected.to have_body_text /#{current_user.name}/i
+            is_expected.to have_html_escaped_body_text current_user.name
           end
 
           it 'contains a link to the issue' do
-            is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+            is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue)
           end
         end
 
@@ -181,7 +190,7 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i
+            is_expected.to have_referable_subject(issue, reply: true)
           end
 
           it 'contains link to new issue' do
@@ -191,7 +200,7 @@ describe Notify do
           end
 
           it 'contains a link to the original issue' do
-            is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+            is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
           end
         end
       end
@@ -212,19 +221,19 @@ describe Notify do
           it_behaves_like 'an unsubscribeable thread'
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+            is_expected.to have_referable_subject(merge_request)
           end
 
           it 'contains a link to the new merge request' do
-            is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/
+            is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
           end
 
           it 'contains the source branch for the merge request' do
-            is_expected.to have_body_text /#{merge_request.source_branch}/
+            is_expected.to have_body_text merge_request.source_branch
           end
 
           it 'contains the target branch for the merge request' do
-            is_expected.to have_body_text /#{merge_request.target_branch}/
+            is_expected.to have_body_text merge_request.target_branch
           end
 
           context 'when enabled email_author_in_body' do
@@ -233,8 +242,8 @@ describe Notify do
             end
 
             it 'contains a link to note author' do
-              is_expected.to have_body_text merge_request.author_name
-              is_expected.to have_body_text /wrote\:/
+              is_expected.to have_html_escaped_body_text merge_request.author_name
+              is_expected.to have_body_text 'wrote:'
             end
           end
         end
@@ -246,7 +255,7 @@ describe Notify do
           it_behaves_like "an unsubscribeable thread"
 
           it 'contains the description' do
-            is_expected.to have_body_text /#{merge_request_with_description.description}/
+            is_expected.to have_html_escaped_body_text merge_request_with_description.description
           end
         end
 
@@ -267,19 +276,19 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+            is_expected.to have_referable_subject(merge_request, reply: true)
           end
 
           it 'contains the name of the previous assignee' do
-            is_expected.to have_body_text /#{previous_assignee.name}/
+            is_expected.to have_html_escaped_body_text previous_assignee.name
           end
 
           it 'contains the name of the new assignee' do
-            is_expected.to have_body_text /#{assignee.name}/
+            is_expected.to have_html_escaped_body_text assignee.name
           end
 
           it 'contains a link to the merge request' do
-            is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+            is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
           end
         end
 
@@ -301,15 +310,15 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+            is_expected.to have_referable_subject(merge_request, reply: true)
           end
 
           it 'contains the names of the added labels' do
-            is_expected.to have_body_text /foo, bar, and baz/
+            is_expected.to have_body_text 'foo, bar, and baz'
           end
 
           it 'contains a link to the merge request' do
-            is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+            is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
           end
         end
 
@@ -330,19 +339,19 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i
+            is_expected.to have_referable_subject(merge_request, reply: true)
           end
 
           it 'contains the new status' do
-            is_expected.to have_body_text /#{status}/i
+            is_expected.to have_body_text status
           end
 
           it 'contains the user name' do
-            is_expected.to have_body_text /#{current_user.name}/i
+            is_expected.to have_html_escaped_body_text current_user.name
           end
 
           it 'contains a link to the merge request' do
-            is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+            is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
           end
         end
 
@@ -363,15 +372,15 @@ describe Notify do
           end
 
           it 'has the correct subject' do
-            is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+            is_expected.to have_referable_subject(merge_request, reply: true)
           end
 
           it 'contains the new status' do
-            is_expected.to have_body_text /merged/i
+            is_expected.to have_body_text 'merged'
           end
 
           it 'contains a link to the merge request' do
-            is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+            is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
           end
         end
       end
@@ -387,15 +396,15 @@ describe Notify do
       it_behaves_like "a user cannot unsubscribe through footer link"
 
       it 'has the correct subject' do
-        is_expected.to have_subject /Project was moved/
+        is_expected.to have_subject "#{project.name} | Project was moved"
       end
 
       it 'contains name of project' do
-        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
       end
 
       it 'contains new user role' do
-        is_expected.to have_body_text /#{project.ssh_url_to_repo}/
+        is_expected.to have_body_text project.ssh_url_to_repo
       end
     end
 
@@ -424,9 +433,9 @@ describe Notify do
           expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
 
           is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
-          is_expected.to have_body_text /#{project.name_with_namespace}/
-          is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
-          is_expected.to have_body_text /#{project_member.human_access}/
+          is_expected.to have_html_escaped_body_text project.name_with_namespace
+          is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
+          is_expected.to have_body_text project_member.human_access
         end
       end
 
@@ -451,9 +460,9 @@ describe Notify do
           expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
 
           is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
-          is_expected.to have_body_text /#{project.name_with_namespace}/
-          is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
-          is_expected.to have_body_text /#{project_member.human_access}/
+          is_expected.to have_html_escaped_body_text project.name_with_namespace
+          is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
+          is_expected.to have_body_text project_member.human_access
         end
       end
     end
@@ -473,13 +482,14 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
-        is_expected.to have_body_text /#{project.name_with_namespace}/
-        is_expected.to have_body_text /#{project.web_url}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
+        is_expected.to have_body_text project.web_url
       end
     end
 
     describe 'project access changed' do
-      let(:project) { create(:empty_project, :public, :access_requestable) }
+      let(:owner) { create(:user, name: "Chang O'Keefe") }
+      let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) }
       let(:user) { create(:user) }
       let(:project_member) { create(:project_member, project: project, user: user) }
       subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -490,9 +500,9 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
-        is_expected.to have_body_text /#{project.name_with_namespace}/
-        is_expected.to have_body_text /#{project.web_url}/
-        is_expected.to have_body_text /#{project_member.human_access}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
+        is_expected.to have_body_text project.web_url
+        is_expected.to have_body_text project_member.human_access
       end
     end
 
@@ -521,10 +531,10 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
-        is_expected.to have_body_text /#{project.name_with_namespace}/
-        is_expected.to have_body_text /#{project.web_url}/
-        is_expected.to have_body_text /#{project_member.human_access}/
-        is_expected.to have_body_text /#{project_member.invite_token}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
+        is_expected.to have_body_text project.web_url
+        is_expected.to have_body_text project_member.human_access
+        is_expected.to have_body_text project_member.invite_token
       end
     end
 
@@ -546,10 +556,10 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject 'Invitation accepted'
-        is_expected.to have_body_text /#{project.name_with_namespace}/
-        is_expected.to have_body_text /#{project.web_url}/
-        is_expected.to have_body_text /#{project_member.invite_email}/
-        is_expected.to have_body_text /#{invited_user.name}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
+        is_expected.to have_body_text project.web_url
+        is_expected.to have_body_text project_member.invite_email
+        is_expected.to have_html_escaped_body_text invited_user.name
       end
     end
 
@@ -570,9 +580,9 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject 'Invitation declined'
-        is_expected.to have_body_text /#{project.name_with_namespace}/
-        is_expected.to have_body_text /#{project.web_url}/
-        is_expected.to have_body_text /#{project_member.invite_email}/
+        is_expected.to have_html_escaped_body_text project.name_with_namespace
+        is_expected.to have_body_text project.web_url
+        is_expected.to have_body_text project_member.invite_email
       end
     end
 
@@ -598,11 +608,11 @@ describe Notify do
         end
 
         it 'contains the message from the note' do
-          is_expected.to have_body_text /#{note.note}/
+          is_expected.to have_html_escaped_body_text note.note
         end
 
         it 'does not contain note author' do
-          is_expected.not_to have_body_text /wrote\:/
+          is_expected.not_to have_body_text 'wrote:'
         end
 
         context 'when enabled email_author_in_body' do
@@ -611,8 +621,8 @@ describe Notify do
           end
 
           it 'contains a link to note author' do
-            is_expected.to have_body_text note.author_name
-            is_expected.to have_body_text /wrote\:/
+            is_expected.to have_html_escaped_body_text note.author_name
+            is_expected.to have_body_text 'wrote:'
           end
         end
       end
@@ -632,7 +642,7 @@ describe Notify do
         it_behaves_like 'a user cannot unsubscribe through footer link'
 
         it 'has the correct subject' do
-          is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/
+          is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})"
         end
 
         it 'contains a link to the commit' do
@@ -655,11 +665,11 @@ describe Notify do
         it_behaves_like 'an unsubscribeable thread'
 
         it 'has the correct subject' do
-          is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+          is_expected.to have_referable_subject(merge_request, reply: true)
         end
 
         it 'contains a link to the merge request note' do
-          is_expected.to have_body_text /#{note_on_merge_request_path}/
+          is_expected.to have_body_text note_on_merge_request_path
         end
       end
 
@@ -678,11 +688,11 @@ describe Notify do
         it_behaves_like 'an unsubscribeable thread'
 
         it 'has the correct subject' do
-          is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+          is_expected.to have_referable_subject(issue, reply: true)
         end
 
         it 'contains a link to the issue note' do
-          is_expected.to have_body_text /#{note_on_issue_path}/
+          is_expected.to have_body_text note_on_issue_path
         end
       end
     end
@@ -698,11 +708,11 @@ describe Notify do
         let(:note) { create(model, project: project, author: note_author) }
 
         it "includes diffs with character-level highlighting" do
-          is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/
+          is_expected.to have_body_text '<span class="p">}</span></span>'
         end
 
         it 'contains a link to the diff file' do
-          is_expected.to have_body_text /#{note.diff_file.file_path}/
+          is_expected.to have_body_text note.diff_file.file_path
         end
 
         it_behaves_like 'it should have Gmail Actions links'
@@ -718,11 +728,11 @@ describe Notify do
         end
 
         it 'contains the message from the note' do
-          is_expected.to have_body_text /#{note.note}/
+          is_expected.to have_html_escaped_body_text note.note
         end
 
         it 'does not contain note author' do
-          is_expected.not_to have_body_text /wrote\:/
+          is_expected.not_to have_body_text 'wrote:'
         end
 
         context 'when enabled email_author_in_body' do
@@ -731,8 +741,8 @@ describe Notify do
           end
 
           it 'contains a link to note author' do
-            is_expected.to have_body_text note.author_name
-            is_expected.to have_body_text /wrote\:/
+            is_expected.to have_html_escaped_body_text note.author_name
+            is_expected.to have_body_text 'wrote:'
           end
         end
       end
@@ -777,9 +787,9 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Request to join the #{group.name} group"
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group_group_members_url(group)}/
-        is_expected.to have_body_text /#{group_member.human_access}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group_group_members_url(group)
+        is_expected.to have_body_text group_member.human_access
       end
     end
 
@@ -798,8 +808,8 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Access to the #{group.name} group was denied"
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group.web_url}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group.web_url
       end
     end
 
@@ -816,9 +826,9 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Access to the #{group.name} group was granted"
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group.web_url}/
-        is_expected.to have_body_text /#{group_member.human_access}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group.web_url
+        is_expected.to have_body_text group_member.human_access
       end
     end
 
@@ -847,10 +857,10 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject "Invitation to join the #{group.name} group"
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group.web_url}/
-        is_expected.to have_body_text /#{group_member.human_access}/
-        is_expected.to have_body_text /#{group_member.invite_token}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group.web_url
+        is_expected.to have_body_text group_member.human_access
+        is_expected.to have_body_text group_member.invite_token
       end
     end
 
@@ -872,10 +882,10 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject 'Invitation accepted'
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group.web_url}/
-        is_expected.to have_body_text /#{group_member.invite_email}/
-        is_expected.to have_body_text /#{invited_user.name}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group.web_url
+        is_expected.to have_body_text group_member.invite_email
+        is_expected.to have_html_escaped_body_text invited_user.name
       end
     end
 
@@ -896,9 +906,9 @@ describe Notify do
 
       it 'contains all the useful information' do
         is_expected.to have_subject 'Invitation declined'
-        is_expected.to have_body_text /#{group.name}/
-        is_expected.to have_body_text /#{group.web_url}/
-        is_expected.to have_body_text /#{group_member.invite_email}/
+        is_expected.to have_html_escaped_body_text group.name
+        is_expected.to have_body_text group.web_url
+        is_expected.to have_body_text group_member.invite_email
       end
     end
   end
@@ -925,11 +935,11 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /^Confirmation instructions/
+      is_expected.to have_subject 'Confirmation instructions | A Nice Suffix'
     end
 
     it 'includes a link to the site' do
-      is_expected.to have_body_text /#{example_site_path}/
+      is_expected.to have_body_text example_site_path
     end
   end
 
@@ -952,11 +962,11 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /Pushed new branch master/
+      is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master"
     end
 
     it 'contains a link to the branch' do
-      is_expected.to have_body_text /#{tree_path}/
+      is_expected.to have_body_text tree_path
     end
   end
 
@@ -979,11 +989,11 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /Pushed new tag v1\.0/
+      is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0"
     end
 
     it 'contains a link to the tag' do
-      is_expected.to have_body_text /#{tree_path}/
+      is_expected.to have_body_text tree_path
     end
   end
 
@@ -1005,7 +1015,7 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /Deleted branch master/
+      is_expected.to have_subject "[Git][#{project.full_path}] Deleted branch master"
     end
   end
 
@@ -1027,7 +1037,7 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /Deleted tag v1\.0/
+      is_expected.to have_subject "[Git][#{project.full_path}] Deleted tag v1.0"
     end
   end
 
@@ -1055,23 +1065,23 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/
+      is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified"
     end
 
     it 'includes commits list' do
-      is_expected.to have_body_text /Change some files/
+      is_expected.to have_body_text 'Change some files'
     end
 
     it 'includes diffs with character-level highlighting' do
-      is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
+      is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex'
     end
 
     it 'contains a link to the diff' do
-      is_expected.to have_body_text /#{diff_path}/
+      is_expected.to have_body_text diff_path
     end
 
     it 'does not contain the misleading footer' do
-      is_expected.not_to have_body_text /you are a member of/
+      is_expected.not_to have_body_text 'you are a member of'
     end
 
     context "when set to send from committer email if domain matches" do
@@ -1157,19 +1167,19 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject /#{commits.first.title}/
+      is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}"
     end
 
     it 'includes commits list' do
-      is_expected.to have_body_text /Change some files/
+      is_expected.to have_body_text 'Change some files'
     end
 
     it 'includes diffs with character-level highlighting' do
-      is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
+      is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex'
     end
 
     it 'contains a link to the diff' do
-      is_expected.to have_body_text /#{diff_path}/
+      is_expected.to have_body_text diff_path
     end
   end
 
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..57eb03e3c809341e2918c8796347266ffc15adf9
--- /dev/null
+++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe MigrateBuildEventsToPipelineEvents, truncate: true do
+  let(:migration) { described_class.new }
+  let(:project_with_pipeline_service) { create(:empty_project) }
+  let(:project_with_build_service) { create(:empty_project) }
+
+  before do
+    ActiveRecord::Base.connection.execute <<-SQL
+      INSERT INTO services (properties, build_events, pipeline_events, type)
+      VALUES
+        ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
+      , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
+      , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
+      ;
+    SQL
+
+    ActiveRecord::Base.connection.execute <<-SQL
+      INSERT INTO services
+        (properties, build_events, pipeline_events, type, project_id)
+      VALUES
+        ('{"notify_only_broken_builds":true}', true, false,
+         'BuildsEmailService', #{project_with_pipeline_service.id})
+      , ('{"notify_only_broken_pipelines":true}', false, true,
+         'PipelinesEmailService', #{project_with_pipeline_service.id})
+      , ('{"notify_only_broken_builds":true}', true, false,
+         'BuildsEmailService', #{project_with_build_service.id})
+      ;
+    SQL
+  end
+
+  describe '#up' do
+    before do
+      silence_migration = Module.new do
+        # rubocop:disable Rails/Delegate
+        def execute(query)
+          connection.execute(query)
+        end
+      end
+
+      migration.extend(silence_migration)
+      migration.up
+    end
+
+    it 'migrates chat service properly' do
+      [SlackService, MattermostService, HipchatService].each do |service|
+        expect(service.count).to eq(1)
+
+        verify_service_record(service.first)
+      end
+    end
+
+    it 'migrates pipelines email service only if it has none before' do
+      Project.find_each do |project|
+        pipeline_service_count =
+          project.services.where(type: 'PipelinesEmailService').count
+
+        expect(pipeline_service_count).to eq(1)
+
+        verify_service_record(project.pipelines_email_service)
+      end
+    end
+
+    def verify_service_record(service)
+      expect(service.notify_only_broken_pipelines).to be(true)
+      expect(service.build_events).to be(false)
+      expect(service.pipeline_events).to be(true)
+    end
+  end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 6a93deb5412e54591fbae25d71418a2c2d05fea6..b6d678bac18db20e65c84d62d5b6a7b59cee8faf 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -62,7 +62,7 @@ describe MigrateProcessCommitWorkerJobs do
     end
 
     def pop_job
-      JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+      JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
     end
 
     before do
@@ -198,7 +198,7 @@ describe MigrateProcessCommitWorkerJobs do
       let(:job) do
         migration.down
 
-        JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+        JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
       end
 
       it 'includes the project ID' do
diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..36e82729c23e421fb0b7ba27d8a05068ba996650
--- /dev/null
+++ b/spec/migrations/rename_more_reserved_project_names_spec.rb
@@ -0,0 +1,47 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserved_project_names.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe RenameMoreReservedProjectNames, truncate: true do
+  let(:migration) { described_class.new }
+  let!(:project) { create(:empty_project) }
+
+  before do
+    project.path = 'artifacts'
+    project.save!(validate: false)
+  end
+
+  describe '#up' do
+    context 'when project repository exists' do
+      before { project.create_repository }
+
+      context 'when no exception is raised' do
+        it 'renames project with reserved names' do
+          migration.up
+
+          expect(project.reload.path).to eq('artifacts0')
+        end
+      end
+
+      context 'when exception is raised during rename' do
+        before do
+          allow(project).to receive(:rename_repo).and_raise(StandardError)
+        end
+
+        it 'captures exception from project rename' do
+          expect { migration.up }.not_to raise_error
+        end
+      end
+    end
+
+    context 'when project repository does not exist' do
+      it 'does not raise error' do
+        expect { migration.up }.not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 30f8fdf91b2204a686b1b7e3d4ef71eb61ac6ea8..92d70cfc64c199f96a675de3314db0454792e569 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,12 @@
 require 'spec_helper'
 
 describe Ability, lib: true do
+  context 'using a nil subject' do
+    it 'is always empty' do
+      expect(Ability.allowed(nil, nil).to_set).to be_empty
+    end
+  end
+
   describe '.can_edit_note?' do
     let(:project) { create(:empty_project) }
     let(:note) { create(:note_on_issue, project: project) }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index c4486a3208266126637380e4bc1dbd930c32033b..4e71597521d382b3606cb18cc50c0155296aa1e7 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
 
 RSpec.describe AbuseReport, type: :model do
   subject     { create(:abuse_report) }
-  let(:user)  { create(:user) }
+  let(:user)  { create(:admin) }
 
   it { expect(subject).to be_valid }
 
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 0b72a2f979b24d442bd44c83b2999313bb29d45f..1060bf3cbf404fab046c044ae10cf0d394d42372 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -7,4 +7,6 @@ RSpec.describe Appearance, type: :model do
 
   it { is_expected.to validate_presence_of(:title) }
   it { is_expected.to validate_presence_of(:description) }
+
+  it { is_expected.to have_many(:uploads).dependent(:destroy) }
 end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b950fcdd81aae02426e796e1ac8193872ad84afa..01ca1584ed2eac1173df507c48752e6bf787d902 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -29,6 +29,40 @@ describe ApplicationSetting, models: true do
       it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
     end
 
+    describe 'default_artifacts_expire_in' do
+      it 'sets an error if it cannot parse' do
+        setting.update(default_artifacts_expire_in: 'a')
+
+        expect_invalid
+      end
+
+      it 'sets an error if it is blank' do
+        setting.update(default_artifacts_expire_in: ' ')
+
+        expect_invalid
+      end
+
+      it 'sets the value if it is valid' do
+        setting.update(default_artifacts_expire_in: '30 days')
+
+        expect(setting).to be_valid
+        expect(setting.default_artifacts_expire_in).to eq('30 days')
+      end
+
+      it 'sets the value if it is 0' do
+        setting.update(default_artifacts_expire_in: '0')
+
+        expect(setting).to be_valid
+        expect(setting.default_artifacts_expire_in).to eq('0')
+      end
+
+      def expect_invalid
+        expect(setting).to be_invalid
+        expect(setting.errors.messages)
+          .to have_key(:default_artifacts_expire_in)
+      end
+    end
+
     it { is_expected.to validate_presence_of(:max_attachment_size) }
 
     it do
@@ -62,9 +96,9 @@ describe ApplicationSetting, models: true do
 
       describe 'inclusion' do
         it { is_expected.to allow_value('custom1').for(:repository_storages) }
-        it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) }
+        it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) }
         it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
-        it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) }
+        it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) }
       end
 
       describe 'presence' do
@@ -83,7 +117,7 @@ describe ApplicationSetting, models: true do
 
         describe '#repository_storage' do
           it 'returns the first storage' do
-            setting.repository_storages = ['good', 'bad']
+            setting.repository_storages = %w(good bad)
 
             expect(setting.repository_storage).to eq('good')
           end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 03d02b4d382bc25ca85cc41ff39dafe77f8f3a98..94c25a454aac24d86ac34b2add9e10fc0d494edc 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -70,6 +70,8 @@ describe Blob do
   end
 
   describe '#to_partial_path' do
+    let(:project) { double(lfs_enabled?: true) }
+
     def stubbed_blob(overrides = {})
       overrides.reverse_merge!(
         image?: false,
@@ -84,34 +86,35 @@ describe Blob do
       end
     end
 
-    it 'handles LFS pointers' do
-      blob = stubbed_blob(lfs_pointer?: true)
+    it 'handles LFS pointers with LFS enabled' do
+      blob = stubbed_blob(lfs_pointer?: true, text?: true)
+      expect(blob.to_partial_path(project)).to eq 'download'
+    end
 
-      expect(blob.to_partial_path).to eq 'download'
+    it 'handles LFS pointers with LFS disabled' do
+      blob = stubbed_blob(lfs_pointer?: true, text?: true)
+      project = double(lfs_enabled?: false)
+      expect(blob.to_partial_path(project)).to eq 'text'
     end
 
     it 'handles SVGs' do
       blob = stubbed_blob(text?: true, svg?: true)
-
-      expect(blob.to_partial_path).to eq 'image'
+      expect(blob.to_partial_path(project)).to eq 'image'
     end
 
     it 'handles images' do
       blob = stubbed_blob(image?: true)
-
-      expect(blob.to_partial_path).to eq 'image'
+      expect(blob.to_partial_path(project)).to eq 'image'
     end
 
     it 'handles text' do
       blob = stubbed_blob(text?: true)
-
-      expect(blob.to_partial_path).to eq 'text'
+      expect(blob.to_partial_path(project)).to eq 'text'
     end
 
     it 'defaults to download' do
       blob = stubbed_blob
-
-      expect(blob.to_partial_path).to eq 'download'
+      expect(blob.to_partial_path(project)).to eq 'download'
     end
   end
 
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5283561a83fbce5b6e2a99760c26a7694aedf3d3
--- /dev/null
+++ b/spec/models/chat_team_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ChatTeam, type: :model do
+  subject { create(:chat_team) }
+
+  # Associations
+  it { is_expected.to belong_to(:namespace) }
+
+  # Validations
+  it { is_expected.to validate_uniqueness_of(:namespace) }
+
+  # Fields
+  it { is_expected.to respond_to(:name) }
+  it { is_expected.to respond_to(:team_id) }
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 83a2efb55b9ae2ede718cb44a9a87130bef2847d..ac47b34b6fc8cb48fddc8383eb1fc74ce0406e02 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -20,6 +20,30 @@ describe Ci::Build, :models do
   it { is_expected.to validate_presence_of :ref }
   it { is_expected.to respond_to :trace_html }
 
+  describe '#actionize' do
+    context 'when build is a created' do
+      before do
+        build.update_column(:status, :created)
+      end
+
+      it 'makes build a manual action' do
+        expect(build.actionize).to be true
+        expect(build.reload).to be_manual
+      end
+    end
+
+    context 'when build is not created' do
+      before do
+        build.update_column(:status, :pending)
+      end
+
+      it 'does not change build status' do
+        expect(build.actionize).to be false
+        expect(build.reload).to be_pending
+      end
+    end
+  end
+
   describe '#any_runners_online?' do
     subject { build.any_runners_online? }
 
@@ -162,11 +186,17 @@ describe Ci::Build, :models do
       is_expected.to be_nil
     end
 
-    it 'when resseting value' do
+    it 'when resetting value' do
       build.artifacts_expire_in = nil
 
       is_expected.to be_nil
     end
+
+    it 'when setting to 0' do
+      build.artifacts_expire_in = '0'
+
+      is_expected.to be_nil
+    end
   end
 
   describe '#commit' do
@@ -175,20 +205,6 @@ describe Ci::Build, :models do
     end
   end
 
-  describe '#create_from' do
-    before do
-      build.status = 'success'
-      build.save
-    end
-    let(:create_from_build) { Ci::Build.create_from build }
-
-    it 'exists a pending task' do
-      expect(Ci::Build.pending.count(:all)).to eq 0
-      create_from_build
-      expect(Ci::Build.pending.count(:all)).to be > 0
-    end
-  end
-
   describe '#depends_on_builds' do
     let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
     let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
@@ -329,11 +345,11 @@ describe Ci::Build, :models do
     describe '#expanded_environment_name' do
       subject { build.expanded_environment_name }
 
-      context 'when environment uses $CI_BUILD_REF_NAME' do
+      context 'when environment uses $CI_COMMIT_REF_NAME' do
         let(:build) do
           create(:ci_build,
                  ref: 'master',
-                 environment: 'review/$CI_BUILD_REF_NAME')
+                 environment: 'review/$CI_COMMIT_REF_NAME')
         end
 
         it { is_expected.to eq('review/master') }
@@ -595,13 +611,21 @@ describe Ci::Build, :models do
         it { is_expected.to be_falsey }
       end
 
-      context 'and build.status is failed' do
+      context 'and build status is failed' do
         before do
           build.status = 'failed'
         end
 
         it { is_expected.to be_truthy }
       end
+
+      context 'when build is a manual action' do
+        before do
+          build.status = 'manual'
+        end
+
+        it { is_expected.to be_falsey }
+      end
     end
   end
 
@@ -690,12 +714,12 @@ describe Ci::Build, :models do
       end
     end
 
-    describe '#manual?' do
+    describe '#action?' do
       before do
         build.update(when: value)
       end
 
-      subject { build.manual? }
+      subject { build.action? }
 
       context 'when is set to manual' do
         let(:value) { 'manual' }
@@ -711,14 +735,50 @@ describe Ci::Build, :models do
     end
   end
 
+  describe '#has_commands?' do
+    context 'when build has commands' do
+      let(:build) do
+        create(:ci_build, commands: 'rspec')
+      end
+
+      it 'has commands' do
+        expect(build).to have_commands
+      end
+    end
+
+    context 'when does not have commands' do
+      context 'when commands are an empty string' do
+        let(:build) do
+          create(:ci_build, commands: '')
+        end
+
+        it 'has no commands' do
+          expect(build).not_to have_commands
+        end
+      end
+
+      context 'when commands are not set at all' do
+        let(:build) do
+          create(:ci_build, commands: nil)
+        end
+
+        it 'has no commands' do
+          expect(build).not_to have_commands
+        end
+      end
+    end
+  end
+
   describe '#has_tags?' do
     context 'when build has tags' do
       subject { create(:ci_build, tag_list: ['tag']) }
+
       it { is_expected.to have_tags }
     end
 
     context 'when build does not have tags' do
       subject { create(:ci_build, tag_list: []) }
+
       it { is_expected.not_to have_tags }
     end
   end
@@ -735,8 +795,8 @@ describe Ci::Build, :models do
 
   describe '#merge_request' do
     def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
-      create(factory, source_project_id: pipeline.gl_project_id,
-                      target_project_id: pipeline.gl_project_id,
+      create(factory, source_project: pipeline.project,
+                      target_project: pipeline.project,
                       source_branch: build.ref,
                       created_at: created_at)
     end
@@ -855,7 +915,7 @@ describe Ci::Build, :models do
     end
 
     context 'referenced with a variable' do
-      let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+      let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") }
 
       it { is_expected.to eq(@environment) }
     end
@@ -1226,23 +1286,25 @@ describe Ci::Build, :models do
       [
         { key: 'CI', value: 'true', public: true },
         { key: 'GITLAB_CI', value: 'true', public: true },
-        { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
-        { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
-        { 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 },
         { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
         { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+        { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+        { key: 'CI_JOB_NAME', value: 'test', public: true },
+        { key: 'CI_JOB_STAGE', value: 'test', public: true },
+        { key: 'CI_JOB_TOKEN', value: build.token, public: false },
+        { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
+        { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
+        { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
         { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
         { key: 'CI_PROJECT_NAME', value: project.path, public: true },
-        { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
-        { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+        { key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
+        { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
         { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
-        { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+        { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+        { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+        { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
+        { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
       ]
     end
 
@@ -1257,13 +1319,13 @@ describe Ci::Build, :models do
         build.yaml_variables = []
       end
 
-      it { is_expected.to eq(predefined_variables) }
+      it { is_expected.to include(*predefined_variables) }
     end
 
     context 'when build has user' do
       let(:user_variables) do
-        [ { key: 'GITLAB_USER_ID',    value: user.id.to_s, public: true },
-          { key: 'GITLAB_USER_EMAIL', value: user.email,   public: true } ]
+        [{ key: 'GITLAB_USER_ID',    value: user.id.to_s, public: true },
+         { key: 'GITLAB_USER_EMAIL', value: user.email,   public: true }]
       end
 
       before do
@@ -1295,7 +1357,7 @@ describe Ci::Build, :models do
       end
 
       let(:manual_variable) do
-        { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+        { key: 'CI_JOB_MANUAL', value: 'true', public: true }
       end
 
       it { is_expected.to include(manual_variable) }
@@ -1303,7 +1365,7 @@ describe Ci::Build, :models do
 
     context 'when build is for tag' do
       let(:tag_variable) do
-        { key: 'CI_BUILD_TAG', value: 'master', public: true }
+        { key: 'CI_COMMIT_TAG', value: 'master', public: true }
       end
 
       before do
@@ -1332,7 +1394,7 @@ describe Ci::Build, :models do
         { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
       end
       let(:predefined_trigger_variable) do
-        { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+        { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
       end
 
       before do
@@ -1356,7 +1418,7 @@ describe Ci::Build, :models do
         context 'when config is not found' do
           let(:config) { nil }
 
-          it { is_expected.to eq(predefined_variables) }
+          it { is_expected.to include(*predefined_variables) }
         end
 
         context 'when config does not have a questioned job' do
@@ -1368,7 +1430,7 @@ describe Ci::Build, :models do
             })
           end
 
-          it { is_expected.to eq(predefined_variables) }
+          it { is_expected.to include(*predefined_variables) }
         end
 
         context 'when config has variables' do
@@ -1386,7 +1448,8 @@ describe Ci::Build, :models do
             [{ key: 'KEY', value: 'value', public: true }]
           end
 
-          it { is_expected.to eq(predefined_variables + variables) }
+          it { is_expected.to include(*predefined_variables) }
+          it { is_expected.to include(*variables) }
         end
       end
     end
@@ -1420,7 +1483,7 @@ describe Ci::Build, :models do
     end
 
     context 'when runner is assigned to build' do
-      let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+      let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
 
       before do
         build.update(runner: runner)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 10c2bfbb4000ee0b6d6d98e42448c1725dc63be3..53282b999dc604e45f17b00992017b47cf62256d 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -24,6 +24,14 @@ describe Ci::Pipeline, models: true do
   it { is_expected.to respond_to :git_author_email }
   it { is_expected.to respond_to :short_sha }
 
+  describe '#block' do
+    it 'changes pipeline status to manual' do
+      expect(pipeline.block).to be true
+      expect(pipeline.reload).to be_manual
+      expect(pipeline.reload).to be_blocked
+    end
+  end
+
   describe '#valid_commit_sha' do
     context 'commit.sha can not start with 00000000' do
       before do
@@ -168,9 +176,9 @@ describe Ci::Pipeline, models: true do
         end
 
         it 'returns list of stages with correct statuses' do
-          expect(statuses).to eq([['build', 'failed'],
-                                  ['test', 'success'],
-                                  ['deploy', 'running']])
+          expect(statuses).to eq([%w(build failed),
+                                  %w(test success),
+                                  %w(deploy running)])
         end
 
         context 'when commit status  is retried' do
@@ -183,12 +191,30 @@ describe Ci::Pipeline, models: true do
           end
 
           it 'ignores the previous state' do
-            expect(statuses).to eq([['build', 'success'],
-                                    ['test', 'success'],
-                                    ['deploy', 'running']])
+            expect(statuses).to eq([%w(build success),
+                                    %w(test success),
+                                    %w(deploy running)])
           end
         end
       end
+
+      context 'when there is a stage with warnings' do
+        before do
+          create(:commit_status, pipeline: pipeline,
+                                 stage: 'deploy',
+                                 name: 'prod:2',
+                                 stage_idx: 2,
+                                 status: 'failed',
+                                 allow_failure: true)
+        end
+
+        it 'populates stage with correct number of warnings' do
+          deploy_stage = pipeline.stages.third
+
+          expect(deploy_stage).not_to receive(:statuses)
+          expect(deploy_stage).to have_warnings
+        end
+      end
     end
 
     describe '#stages_count' do
@@ -199,7 +225,7 @@ describe Ci::Pipeline, models: true do
 
     describe '#stages_name' do
       it 'returns a valid names of stages' do
-        expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
+        expect(pipeline.stages_name).to eq(%w(build test deploy))
       end
     end
   end
@@ -506,6 +532,19 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  describe '.latest_successful_for_refs' do
+    include_context 'with some outdated pipelines'
+
+    let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
+    let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+
+    it 'returns the latest successful pipeline for both refs' do
+      refs = %w(ref1 ref2 ref3)
+
+      expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 })
+    end
+  end
+
   describe '#status' do
     let(:build) do
       create(:ci_build, :created, pipeline: pipeline, name: 'test')
@@ -635,6 +674,14 @@ describe Ci::Pipeline, models: true do
       end
     end
 
+    context 'when pipeline is blocked' do
+      let(:pipeline) { create(:ci_pipeline, status: :manual) }
+
+      it 'returns detailed status for blocked pipeline' do
+        expect(subject.text).to eq 'blocked'
+      end
+    end
+
     context 'when pipeline is successful but with warnings' do
       let(:pipeline) { create(:ci_pipeline, status: :success) }
 
@@ -767,7 +814,7 @@ describe Ci::Pipeline, models: true do
       end
 
       it 'cancels created builds' do
-        expect(latest_status).to eq ['canceled', 'canceled']
+        expect(latest_status).to eq %w(canceled canceled)
       end
     end
   end
@@ -984,6 +1031,19 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  describe '#update_status' do
+    let(:pipeline) { create(:ci_pipeline, sha: '123456') }
+
+    it 'updates the cached status' do
+      fake_status = double
+      # after updating the status, the status is set to `skipped` for this pipeline's builds
+      expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
+      expect(fake_status).to receive(:store_in_cache_if_needed)
+
+      pipeline.update_status
+    end
+  end
+
   describe 'notifications when pipeline success or failed' do
     let(:project) { create(:project, :repository) }
 
diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/models/ci/pipeline_status_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bc5b71666c27e637825360780a0c76ff0effd58e
--- /dev/null
+++ b/spec/models/ci/pipeline_status_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Ci::PipelineStatus do
+  let(:project) { create(:project) }
+  let(:pipeline_status) { described_class.new(project) }
+
+  describe '.load_for_project' do
+    it "loads the status" do
+      expect_any_instance_of(described_class).to receive(:load_status)
+
+      described_class.load_for_project(project)
+    end
+  end
+
+  describe '#has_status?' do
+    it "is false when the status wasn't loaded yet" do
+      expect(pipeline_status.has_status?).to be_falsy
+    end
+
+    it 'is true when all status information was loaded' do
+      fake_commit = double
+      allow(fake_commit).to receive(:status).and_return('failed')
+      allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
+      allow(pipeline_status).to receive(:commit).and_return(fake_commit)
+      allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+      pipeline_status.load_status
+
+      expect(pipeline_status.has_status?).to be_truthy
+    end
+  end
+
+  describe '#load_status' do
+    it 'loads the status from the cache when there is one' do
+      expect(pipeline_status).to receive(:has_cache?).and_return(true)
+      expect(pipeline_status).to receive(:load_from_cache)
+
+      pipeline_status.load_status
+    end
+
+    it 'loads the status from the project commit when there is no cache' do
+      allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+      expect(pipeline_status).to receive(:load_from_commit)
+
+      pipeline_status.load_status
+    end
+
+    it 'stores the status in the cache when it loading it from the project' do
+      allow(pipeline_status).to receive(:has_cache?).and_return(false)
+      allow(pipeline_status).to receive(:load_from_commit)
+
+      expect(pipeline_status).to receive(:store_in_cache)
+
+      pipeline_status.load_status
+    end
+
+    it 'sets the state to loaded' do
+      pipeline_status.load_status
+
+      expect(pipeline_status).to be_loaded
+    end
+
+    it 'only loads the status once' do
+      expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
+      expect(pipeline_status).to receive(:load_from_cache).exactly(1)
+
+      pipeline_status.load_status
+      pipeline_status.load_status
+    end
+  end
+
+  describe "#load_from_commit" do
+    let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+    it 'reads the status from the pipeline for the commit' do
+      pipeline_status.load_from_commit
+
+      expect(pipeline_status.status).to eq('success')
+      expect(pipeline_status.sha).to eq(project.commit.sha)
+    end
+
+    it "doesn't fail for an empty project" do
+      status_for_empty_commit = described_class.new(create(:empty_project))
+
+      status_for_empty_commit.load_status
+
+      expect(status_for_empty_commit).to be_loaded
+    end
+  end
+
+  describe "#store_in_cache", :redis do
+    it "sets the object in redis" do
+      pipeline_status.sha = '123456'
+      pipeline_status.status = 'failed'
+
+      pipeline_status.store_in_cache
+      read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+      expect(read_sha).to eq('123456')
+      expect(read_status).to eq('failed')
+    end
+  end
+
+  describe '#store_in_cache_if_needed', :redis do
+    it 'stores the state in the cache when the sha is the HEAD of the project' do
+      create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+      build_status = described_class.load_for_project(project)
+
+      build_status.store_in_cache_if_needed
+      sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+      expect(sha).not_to be_nil
+      expect(status).not_to be_nil
+    end
+
+    it "doesn't store the status in redis when the sha is not the head of the project" do
+      other_status = described_class.new(project, sha: "123456", status: "failed")
+
+      other_status.store_in_cache_if_needed
+      sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+      expect(sha).to be_nil
+      expect(status).to be_nil
+    end
+
+    it "deletes the cache if the repository doesn't have a head commit" do
+      empty_project = create(:empty_project)
+      Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
+      other_status = described_class.new(empty_project, sha: "123456", status: "failed")
+
+      other_status.store_in_cache_if_needed
+      sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
+
+      expect(sha).to be_nil
+      expect(status).to be_nil
+    end
+  end
+
+  describe "with a status in redis", :redis do
+    let(:status) { 'success' }
+    let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+
+    before do
+      Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
+    end
+
+    describe '#load_from_cache' do
+      it 'reads the status from redis' do
+        pipeline_status.load_from_cache
+
+        expect(pipeline_status.sha).to eq(sha)
+        expect(pipeline_status.status).to eq(status)
+      end
+    end
+
+    describe '#has_cache?' do
+      it 'knows the status is cached' do
+        expect(pipeline_status.has_cache?).to be_truthy
+      end
+    end
+
+    describe '#delete_from_cache' do
+      it 'deletes values from redis'  do
+        pipeline_status.delete_from_cache
+
+        key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
+
+        expect(key_exists).to be_falsy
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index f8513ac8b1c1ba6d134a670ba40313c4ee95b7aa..76ce558eea0a39e2e8d30ba579ee2a40880796ee 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -113,7 +113,7 @@ describe Ci::Runner, models: true do
 
     context 'when runner has tags' do
       before do
-        runner.tag_list = ['bb', 'cc']
+        runner.tag_list = %w(bb cc)
       end
 
       shared_examples 'tagged build picker' do
@@ -169,7 +169,7 @@ describe Ci::Runner, models: true do
 
         context 'when having runner tags' do
           before do
-            runner.tag_list = ['bb', 'cc']
+            runner.tag_list = %w(bb cc)
           end
 
           it 'cannot handle it for builds without matching tags' do
@@ -189,7 +189,7 @@ describe Ci::Runner, models: true do
 
         context 'when having runner tags' do
           before do
-            runner.tag_list = ['bb', 'cc']
+            runner.tag_list = %w(bb cc)
             build.tag_list = ['bb']
           end
 
@@ -212,7 +212,7 @@ describe Ci::Runner, models: true do
 
         context 'when having runner tags' do
           before do
-            runner.tag_list = ['bb', 'cc']
+            runner.tag_list = %w(bb cc)
             build.tag_list = ['bb']
           end
 
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index c4a9743a4e245b8d6acd96ed61d652d5a2fac15e..c38faf32f7df3ad7c5fed5a805b75db70eee0991 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -170,22 +170,31 @@ describe Ci::Stage, models: true do
     context 'when stage has warnings' do
       context 'when using memoized warnings flag' do
         context 'when there are warnings' do
-          let(:stage) { build(:ci_stage, warnings: true) }
+          let(:stage) { build(:ci_stage, warnings: 2) }
 
-          it 'has memoized warnings' do
+          it 'returns true using memoized value' do
             expect(stage).not_to receive(:statuses)
             expect(stage).to have_warnings
           end
         end
 
         context 'when there are no warnings' do
-          let(:stage) { build(:ci_stage, warnings: false) }
+          let(:stage) { build(:ci_stage, warnings: 0) }
 
-          it 'has memoized warnings' do
+          it 'returns false using memoized value' do
             expect(stage).not_to receive(:statuses)
             expect(stage).not_to have_warnings
           end
         end
+
+        context 'when number of warnings is not a valid value' do
+          let(:stage) { build(:ci_stage, warnings: true) }
+
+          it 'calculates statuses using database queries' do
+            expect(stage).to receive(:statuses).and_call_original
+            expect(stage).not_to have_warnings
+          end
+        end
       end
 
       context 'when calculating warnings from statuses' do
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 3ca9231f58e3ea88cafbc7ee858f8ef5d91fdb39..1bcb673cb16438339456082b4d9f3aa83f3b80c1 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,17 +1,83 @@
 require 'spec_helper'
 
 describe Ci::Trigger, models: true do
-  let(:project) { FactoryGirl.create :empty_project }
+  let(:project) { create :empty_project }
+
+  describe 'associations' do
+    it { is_expected.to belong_to(:project) }
+    it { is_expected.to belong_to(:owner) }
+    it { is_expected.to have_many(:trigger_requests) }
+  end
 
   describe 'before_validation' do
     it 'sets an random token if none provided' do
-      trigger = FactoryGirl.create :ci_trigger_without_token, project: project
+      trigger = create(:ci_trigger_without_token, project: project)
+
       expect(trigger.token).not_to be_nil
     end
 
     it 'does not set an random token if one provided' do
-      trigger = FactoryGirl.create :ci_trigger, project: project
+      trigger = create(:ci_trigger, project: project)
+
       expect(trigger.token).to eq('token')
     end
   end
+
+  describe '#short_token' do
+    let(:trigger) { create(:ci_trigger, token: '12345678') }
+
+    subject { trigger.short_token }
+
+    it 'returns shortened token' do
+      is_expected.to eq('1234')
+    end
+  end
+
+  describe '#legacy?' do
+    let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
+
+    subject { trigger }
+
+    context 'when owner is blank' do
+      let(:owner) { nil }
+
+      it { is_expected.to be_legacy }
+    end
+
+    context 'when owner is set' do
+      let(:owner) { create(:user) }
+
+      it { is_expected.not_to be_legacy }
+    end
+  end
+
+  describe '#can_access_project?' do
+    let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
+
+    context 'when owner is blank' do
+      let(:owner) { nil }
+
+      subject { trigger.can_access_project? }
+
+      it { is_expected.to eq(true) }
+    end
+
+    context 'when owner is set' do
+      let(:owner) { create(:user) }
+
+      subject { trigger.can_access_project? }
+
+      context 'and is member of the project' do
+        before do
+          project.team << [owner, :developer]
+        end
+
+        it { is_expected.to eq(true) }
+      end
+
+      context 'and is not member of the project' do
+        it { is_expected.to eq(false) }
+      end
+    end
+  end
 end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index bee9f7148497e91c2e59ff3f3b84f99cb208fc20..048d25869bc92e2eb202457d55415279345f2796 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable, models: true do
   let(:secret_value) { 'secret' }
 
   it { is_expected.to validate_presence_of(:key) }
-  it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+  it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
   it { is_expected.to validate_length_of(:key).is_at_most(255) }
   it { is_expected.to allow_value('foo').for(:key) }
   it { is_expected.not_to allow_value('foo bar').for(:key) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 32f9366a14cc1de4d92a7bb7c5d7f580951549e1..980a1b70ef5a636e5b8d6b6299b00c8ba6cc497f 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -212,6 +212,25 @@ eos
     end
   end
 
+  describe '#latest_pipeline' do
+    let!(:first_pipeline) do
+      create(:ci_empty_pipeline,
+        project: project,
+        sha: commit.sha,
+        status: 'success')
+    end
+    let!(:second_pipeline) do
+      create(:ci_empty_pipeline,
+        project: project,
+        sha: commit.sha,
+        status: 'success')
+    end
+
+    it 'returns latest pipeline' do
+      expect(commit.latest_pipeline).to eq second_pipeline
+    end
+  end
+
   describe '#status' do
     context 'without ref argument' do
       before do
@@ -369,4 +388,32 @@ eos
       expect(described_class.valid_hash?('a' * 41)).to be false
     end
   end
+
+  describe '#raw_diffs' do
+    context 'Gitaly commit_raw_diffs feature enabled' do
+      before do
+        allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+      end
+
+      context 'when a truthy deltas_only is not passed to args' do
+        it 'fetches diffs from Gitaly server' do
+          expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+            with(commit)
+
+          commit.raw_diffs
+        end
+      end
+
+      context 'when a truthy deltas_only is passed to args' do
+        it 'fetches diffs using Rugged' do
+          opts = { deltas_only: true }
+
+          expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+          expect(commit.raw).to receive(:diffs).with(opts)
+
+          commit.raw_diffs(opts)
+        end
+      end
+    end
+  end
 end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 36533bdd11e32103c0bfcabafee19eac4fec896a..ea5e4e210398ef9aaed35f90c99f4d5e40933216 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -158,7 +158,7 @@ describe CommitStatus, :models do
     end
   end
 
-  describe '.exclude_ignored' do
+  describe '.after_stage' do
     subject { described_class.after_stage(0) }
 
     let(:statuses) do
@@ -185,11 +185,32 @@ describe CommitStatus, :models do
        create_status(allow_failure: true, status: 'success'),
        create_status(allow_failure: true, status: 'failed'),
        create_status(allow_failure: false, status: 'success'),
-       create_status(allow_failure: false, status: 'failed')]
+       create_status(allow_failure: false, status: 'failed'),
+       create_status(allow_failure: true, status: 'manual'),
+       create_status(allow_failure: false, status: 'manual')]
+    end
+
+    it 'returns statuses without what we want to ignore' do
+      is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
+    end
+  end
+
+  describe '.failed_but_allowed' do
+    subject { described_class.failed_but_allowed.order(:id) }
+
+    let(:statuses) do
+      [create_status(allow_failure: true, status: 'success'),
+       create_status(allow_failure: true, status: 'failed'),
+       create_status(allow_failure: false, status: 'success'),
+       create_status(allow_failure: false, status: 'failed'),
+       create_status(allow_failure: true, status: 'canceled'),
+       create_status(allow_failure: false, status: 'canceled'),
+       create_status(allow_failure: true, status: 'manual'),
+       create_status(allow_failure: false, status: 'manual')]
     end
 
     it 'returns statuses without what we want to ignore' do
-      is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9))
+      is_expected.to eq(statuses.values_at(1, 4))
     end
   end
 
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 2e3702f7520c401a9094bba865db4cc481efef10..6151d53cd91e71b101840c6e50c0d17cc0e7163f 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,7 +1,8 @@
 require 'spec_helper'
 
 describe CacheMarkdownField do
-  CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+  caching_classes = CacheMarkdownField::CACHING_CLASSES
+  CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze
 
   # The minimum necessary ActiveModel to test this concern
   class ThingWithMarkdownFields
@@ -54,7 +55,7 @@ describe CacheMarkdownField do
     end
   end
 
-  CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+  CacheMarkdownField::CACHING_CLASSES = caching_classes
 
   def thing_subclass(new_attr)
     Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index dbfe3cd2d3631e16d6676c5bf10a733a78b1f9cf..82abad0e2f6c3915b54a1498d1bcf050f91b00f3 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -109,6 +109,42 @@ describe HasStatus do
 
         it { is_expected.to eq 'running' }
       end
+
+      context 'when one status finished and second is still created' do
+        let!(:statuses) do
+          [create(type, status: :success), create(type, status: :created)]
+        end
+
+        it { is_expected.to eq 'running' }
+      end
+
+      context 'when there is a manual status before created status' do
+        let!(:statuses) do
+          [create(type, status: :success),
+           create(type, status: :manual, allow_failure: false),
+           create(type, status: :created)]
+        end
+
+        it { is_expected.to eq 'manual' }
+      end
+
+      context 'when one status is a blocking manual action' do
+        let!(:statuses) do
+          [create(type, status: :failed),
+           create(type, status: :manual, allow_failure: false)]
+        end
+
+        it { is_expected.to eq 'manual' }
+      end
+
+      context 'when one status is a non-blocking manual action' do
+        let!(:statuses) do
+          [create(type, status: :failed),
+           create(type, status: :manual, allow_failure: true)]
+        end
+
+        it { is_expected.to eq 'failed' }
+      end
     end
 
     context 'ci build statuses' do
@@ -218,6 +254,18 @@ describe HasStatus do
         it_behaves_like 'not containing the job', status
       end
     end
+
+    describe '.manual' do
+      subject { CommitStatus.manual }
+
+      %i[manual].each do |status|
+        it_behaves_like 'containing the job', status
+      end
+
+      %i[failed success skipped canceled].each do |status|
+        it_behaves_like 'not containing the job', status
+      end
+    end
   end
 
   describe '::DEFAULT_STATUS' do
@@ -225,4 +273,10 @@ describe HasStatus do
       expect(described_class::DEFAULT_STATUS).to eq 'created'
     end
   end
+
+  describe '::BLOCKED_STATUS' do
+    it 'is a status manual' do
+      expect(described_class::BLOCKED_STATUS).to eq 'manual'
+    end
+  end
 end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 545a11912e37c0805fa6165a03fbd5048905849f..9574796a9455cd7bccfbf07565079853323394ca 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -278,6 +278,16 @@ describe Issue, "Issuable" do
       end
     end
 
+    context 'issue has labels' do
+      let(:labels) { [create(:label), create(:label)] }
+
+      before { issue.update_attribute(:labels, labels)}
+
+      it 'includes labels in the hook data' do
+        expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+      end
+    end
+
     include_examples 'project hook data'
     include_examples 'deprecated repository hook data'
   end
@@ -344,6 +354,46 @@ describe Issue, "Issuable" do
     end
   end
 
+  describe '.order_due_date_and_labels_priority' do
+    let(:project) { create(:empty_project) }
+
+    def create_issue(milestone, labels)
+      create(:labeled_issue, milestone: milestone, labels: labels, project: project)
+    end
+
+    it 'sorts issues in order of milestone due date, then label priority' do
+      first_priority = create(:label, project: project, priority: 1)
+      second_priority = create(:label, project: project, priority: 2)
+      no_priority = create(:label, project: project)
+
+      first_milestone = create(:milestone, project: project, due_date: Time.now)
+      second_milestone = create(:milestone, project: project, due_date: Time.now + 1.month)
+      third_milestone = create(:milestone, project: project)
+
+      # The issues here are ordered by label priority, to ensure that we don't
+      # accidentally just sort by creation date.
+      second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority])
+      third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority])
+      first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority])
+      second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority])
+      no_milestone_second_priority = create_issue(nil, [second_priority, no_priority])
+      first_milestone_no_priority = create_issue(first_milestone, [no_priority])
+      second_milestone_no_labels = create_issue(second_milestone, [])
+      third_milestone_no_priority = create_issue(third_milestone, [no_priority])
+
+      result = Issue.order_due_date_and_labels_priority
+
+      expect(result).to eq([first_milestone_second_priority,
+                            first_milestone_no_priority,
+                            second_milestone_first_priority,
+                            second_milestone_second_priority,
+                            second_milestone_no_labels,
+                            third_milestone_first_priority,
+                            no_milestone_second_priority,
+                            third_milestone_no_priority])
+    end
+  end
+
   describe '.order_labels_priority' do
     let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
     let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index ad703a6c8bbb43cda2cfdc9e3a79ca36a57aaae5..68e4c0a522bc891df6c958c6fed49260ee0e70a1 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -116,21 +116,41 @@ describe Milestone, 'Milestoneish' do
     end
   end
 
+  describe '#remaining_days' do
+    it 'shows 0 if no due date' do
+      milestone = build_stubbed(:milestone)
+
+      expect(milestone.remaining_days).to eq(0)
+    end
+
+    it 'shows 0 if expired' do
+      milestone = build_stubbed(:milestone, due_date: 2.days.ago)
+
+      expect(milestone.remaining_days).to eq(0)
+    end
+
+    it 'shows correct remaining days' do
+      milestone = build_stubbed(:milestone, due_date: 2.days.from_now)
+
+      expect(milestone.remaining_days).to eq(2)
+    end
+  end
+
   describe '#elapsed_days' do
     it 'shows 0 if no start_date set' do
-      milestone = build(:milestone)
+      milestone = build_stubbed(:milestone)
 
       expect(milestone.elapsed_days).to eq(0)
     end
 
     it 'shows 0 if start_date is a future' do
-      milestone = build(:milestone, start_date: Time.now + 2.days)
+      milestone = build_stubbed(:milestone, start_date: Time.now + 2.days)
 
       expect(milestone.elapsed_days).to eq(0)
     end
 
     it 'shows correct amount of days' do
-      milestone = build(:milestone, start_date: Time.now - 2.days)
+      milestone = build_stubbed(:milestone, start_date: Time.now - 2.days)
 
       expect(milestone.elapsed_days).to eq(2)
     end
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..255b584a85e78ec4eb039e0c3c21b14c0365384f
--- /dev/null
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -0,0 +1,204 @@
+require 'spec_helper'
+
+describe Issue, 'RelativePositioning' do
+  let(:project) { create(:empty_project) }
+  let(:issue) { create(:issue, project: project) }
+  let(:issue1) { create(:issue, project: project) }
+  let(:new_issue) { create(:issue, project: project) }
+
+  before do
+    [issue, issue1].each do |issue|
+      issue.move_to_end && issue.save
+    end
+  end
+
+  describe '#max_relative_position' do
+    it 'returns maximum position' do
+      expect(issue.max_relative_position).to eq issue1.relative_position
+    end
+  end
+
+  describe '#prev_relative_position' do
+    it 'returns previous position if there is an issue above' do
+      expect(issue1.prev_relative_position).to eq issue.relative_position
+    end
+
+    it 'returns nil if there is no issue above' do
+      expect(issue.prev_relative_position).to eq nil
+    end
+  end
+
+  describe '#next_relative_position' do
+    it 'returns next position if there is an issue below' do
+      expect(issue.next_relative_position).to eq issue1.relative_position
+    end
+
+    it 'returns nil if there is no issue below' do
+      expect(issue1.next_relative_position).to eq nil
+    end
+  end
+
+  describe '#move_before' do
+    it 'moves issue before' do
+      [issue1, issue].each(&:move_to_end)
+
+      issue.move_before(issue1)
+
+      expect(issue.relative_position).to be < issue1.relative_position
+    end
+  end
+
+  describe '#move_after' do
+    it 'moves issue after' do
+      [issue, issue1].each(&:move_to_end)
+
+      issue.move_after(issue1)
+
+      expect(issue.relative_position).to be > issue1.relative_position
+    end
+  end
+
+  describe '#move_to_end' do
+    it 'moves issue to the end' do
+      new_issue.move_to_end
+
+      expect(new_issue.relative_position).to be > issue1.relative_position
+    end
+  end
+
+  describe '#shift_after?' do
+    it 'returns true' do
+      issue.update(relative_position: issue1.relative_position - 1)
+
+      expect(issue.shift_after?).to be_truthy
+    end
+
+    it 'returns false' do
+      issue.update(relative_position: issue1.relative_position - 2)
+
+      expect(issue.shift_after?).to be_falsey
+    end
+  end
+
+  describe '#shift_before?' do
+    it 'returns true' do
+      issue.update(relative_position: issue1.relative_position + 1)
+
+      expect(issue.shift_before?).to be_truthy
+    end
+
+    it 'returns false' do
+      issue.update(relative_position: issue1.relative_position + 2)
+
+      expect(issue.shift_before?).to be_falsey
+    end
+  end
+
+  describe '#move_between' do
+    it 'positions issue between two other' do
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to be > issue.relative_position
+      expect(new_issue.relative_position).to be < issue1.relative_position
+    end
+
+    it 'positions issue between on top' do
+      new_issue.move_between(nil, issue)
+
+      expect(new_issue.relative_position).to be < issue.relative_position
+    end
+
+    it 'positions issue between to end' do
+      new_issue.move_between(issue1, nil)
+
+      expect(new_issue.relative_position).to be > issue1.relative_position
+    end
+
+    it 'positions issues even when after and before positions are the same' do
+      issue1.update relative_position: issue.relative_position
+
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to be > issue.relative_position
+      expect(issue.relative_position).to be < issue1.relative_position
+    end
+
+    it 'positions issues between other two if distance is 1' do
+      issue1.update relative_position: issue.relative_position + 1
+
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to be > issue.relative_position
+      expect(issue.relative_position).to be < issue1.relative_position
+    end
+
+    it 'positions issue in the middle of other two if distance is big enough' do
+      issue.update relative_position: 6000
+      issue1.update relative_position: 10000
+
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to eq(8000)
+    end
+
+    it 'positions issue closer to the middle if we are at the very top' do
+      issue1.update relative_position: 6000
+
+      new_issue.move_between(nil, issue1)
+
+      expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
+    end
+
+    it 'positions issue closer to the middle if we are at the very bottom' do
+      issue.update relative_position: 6000
+      issue1.update relative_position: nil
+
+      new_issue.move_between(issue, nil)
+
+      expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
+    end
+
+    it 'positions issue in the middle of other two if distance is not big enough' do
+      issue.update relative_position: 100
+      issue1.update relative_position: 400
+
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to eq(250)
+    end
+
+    it 'positions issue in the middle of other two is there is no place' do
+      issue.update relative_position: 100
+      issue1.update relative_position: 101
+
+      new_issue.move_between(issue, issue1)
+
+      expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
+    end
+
+    it 'uses rebalancing if there is no place' do
+      issue.update relative_position: 100
+      issue1.update relative_position: 101
+      issue2 = create(:issue, relative_position: 102, project: project)
+      new_issue.update relative_position: 103
+
+      new_issue.move_between(issue1, issue2)
+      new_issue.save!
+
+      expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+      expect(issue.reload.relative_position).not_to eq(100)
+    end
+
+    it 'positions issue right if we pass none-sequential parameters' do
+      issue.update relative_position: 99
+      issue1.update relative_position: 101
+      issue2 = create(:issue, relative_position: 102, project: project)
+      new_issue.update relative_position: 103
+
+      new_issue.move_between(issue, issue2)
+      new_issue.save!
+
+      expect(new_issue.relative_position).to be(100)
+    end
+  end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index e008ec28fa4518c1118bc39929686fce160deb51..677e60e12822976343a7b0bc696e8012696f90e7 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -86,7 +86,7 @@ describe Group, 'Routable' do
     let(:nested_group) { create(:group, parent: group) }
 
     it { expect(group.full_path).to eq(group.path) }
-    it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
+    it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
   end
 
   describe '#full_name' do
@@ -102,7 +102,7 @@ describe Project, 'Routable' do
   describe '#full_path' do
     let(:project) { build_stubbed(:empty_project) }
 
-    it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" }
+    it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
   end
 
   describe '#full_name' do
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83187d732e402ab9ebd4774af14e51ab6ffcfe9d
--- /dev/null
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Uniquify, models: true do
+  let(:uniquify) { described_class.new }
+
+  describe "#string" do
+    it 'returns the given string if it does not exist' do
+      result = uniquify.string('test_string') { |s| false }
+
+      expect(result).to eq('test_string')
+    end
+
+    it 'returns the given string with a counter attached if the string exists' do
+      result = uniquify.string('test_string') { |s| s == 'test_string' }
+
+      expect(result).to eq('test_string1')
+    end
+
+    it 'increments the counter for each candidate string that also exists' do
+      result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' }
+
+      expect(result).to eq('test_string2')
+    end
+
+    it 'allows passing in a base function that defines the location of the counter' do
+      result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
+        s == 'test__string'
+      end
+
+      expect(result).to eq('test_1_string')
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index b9fe492fe2c790b679dc0dc2a9536be94ee1dbb4..e6a826a9418c8b4a29f0d197d43cbd2f0de682c6 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do
        ["production deploy happens after merge request is merged (along with other changes)",
         lambda do |context, data|
           # Make other changes on master
-          sha = context.project.repository.commit_file(
+          sha = context.project.repository.create_file(
             context.user,
             context.random_git_name,
             'content',
             message: 'commit message',
-            branch_name: 'master',
-            update: false)
+            branch_name: 'master')
           context.project.repository.commit(sha)
 
           context.deploy_master
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 9a024d533a16066e7300cf7ddcccb0f66209ec25..3a02ed81adb5f5f10849262968dd302cd87cb229 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -18,7 +18,7 @@ describe 'CycleAnalytics#staging', feature: true do
     start_time_conditions: [["merge request that closes issue is merged",
                              -> (context, data) do
                                context.merge_merge_requests_closing_issue(data[:issue])
-                             end ]],
+                             end]],
     end_time_conditions:   [["merge request that closes issue is deployed to production",
                              -> (context, data) do
                                context.deploy_master
@@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do
                             ["production deploy happens after merge request is merged (along with other changes)",
                              lambda do |context, data|
                                # Make other changes on master
-                               sha = context.project.repository.commit_file(
+                               sha = context.project.repository.create_file(
                                  context.user,
                                  context.random_git_name,
                                  'content',
                                  message: 'commit message',
-                                 branch_name: 'master',
-                                 update: false)
+                                 branch_name: 'master')
                                context.project.repository.commit(sha)
 
                                context.deploy_master
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index f0ed0c679d5acdaf6820b499c9101ecae96129c5..9f0e7fbbe268c20c7c540a049882064b0af075fd 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -76,7 +76,8 @@ describe Environment, models: true do
   end
 
   describe '#update_merge_request_metrics?' do
-    { 'production' => true,
+    {
+      'production' => true,
       'production/eu' => true,
       'production/www.gitlab.com' => true,
       'productioneu' => false,
@@ -238,7 +239,7 @@ describe Environment, models: true do
   describe '#actions_for' do
     let(:deployment) { create(:deployment, environment: environment) }
     let(:pipeline) { deployment.deployable.pipeline }
-    let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
+    let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )}
     let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
 
     it 'returns a list of actions with matching environment' do
@@ -270,7 +271,11 @@ describe Environment, models: true do
 
     context 'when the environment is unavailable' do
       let(:project) { create(:kubernetes_project) }
-      before { environment.stop }
+
+      before do
+        environment.stop
+      end
+
       it { is_expected.to be_falsy }
     end
   end
@@ -280,20 +285,85 @@ describe Environment, models: true do
     subject { environment.terminals }
 
     context 'when the environment has terminals' do
-      before { allow(environment).to receive(:has_terminals?).and_return(true) }
+      before do
+        allow(environment).to receive(:has_terminals?).and_return(true)
+      end
 
       it 'returns the terminals from the deployment service' do
-        expect(project.deployment_service).
-          to receive(:terminals).with(environment).
-          and_return(:fake_terminals)
+        expect(project.deployment_service)
+          .to receive(:terminals).with(environment)
+          .and_return(:fake_terminals)
 
         is_expected.to eq(:fake_terminals)
       end
     end
 
     context 'when the environment does not have terminals' do
-      before { allow(environment).to receive(:has_terminals?).and_return(false) }
-      it { is_expected.to eq(nil) }
+      before do
+        allow(environment).to receive(:has_terminals?).and_return(false)
+      end
+
+      it { is_expected.to be_nil }
+    end
+  end
+
+  describe '#has_metrics?' do
+    subject { environment.has_metrics? }
+
+    context 'when the enviroment is available' do
+      context 'with a deployment service' do
+        let(:project) { create(:prometheus_project) }
+
+        context 'and a deployment' do
+          let!(:deployment) { create(:deployment, environment: environment) }
+          it { is_expected.to be_truthy }
+        end
+
+        context 'but no deployments' do
+          it { is_expected.to be_falsy }
+        end
+      end
+
+      context 'without a monitoring service' do
+        it { is_expected.to be_falsy }
+      end
+    end
+
+    context 'when the environment is unavailable' do
+      let(:project) { create(:prometheus_project) }
+
+      before do
+        environment.stop
+      end
+
+      it { is_expected.to be_falsy }
+    end
+  end
+
+  describe '#metrics' do
+    let(:project) { create(:prometheus_project) }
+    subject { environment.metrics }
+
+    context 'when the environment has metrics' do
+      before do
+        allow(environment).to receive(:has_metrics?).and_return(true)
+      end
+
+      it 'returns the metrics from the deployment service' do
+        expect(project.monitoring_service)
+          .to receive(:metrics).with(environment)
+          .and_return(:fake_metrics)
+
+        is_expected.to eq(:fake_metrics)
+      end
+    end
+
+    context 'when the environment does not have metrics' do
+      before do
+        allow(environment).to receive(:has_metrics?).and_return(false)
+      end
+
+      it { is_expected.to be_nil }
     end
   end
 
@@ -311,7 +381,7 @@ describe Environment, models: true do
   end
 
   describe '#generate_slug' do
-    SUFFIX = "-[a-z0-9]{6}"
+    SUFFIX = "-[a-z0-9]{6}".freeze
     {
       "staging-12345678901234567" => "staging-123456789" + SUFFIX,
       "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 2debe1289a3f748fb5c2ad70b4f6f99a9f0a0094..cd50bda8996bee9250317880e102b056314ebf68 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -42,4 +42,12 @@ describe ExternalIssue, models: true do
       expect(issue.project_id).to eq(project.id)
     end
   end
+
+  describe '#hash' do
+    it 'returns the hash of its [class, to_s] pair' do
+      issue_2 = described_class.new(issue.to_s, project)
+
+      expect(issue.hash).to eq(issue_2.hash)
+    end
+  end
 end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index cacbab8bcb1a3a3d767057f52027a6d00648e113..55b87d1c48a35cffa623758dbd47f9ace15e81c3 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do
     end
   end
 
+  describe '.states_count' do
+    context 'when the projects have milestones' do
+      before do
+        create(:closed_milestone, title: 'Active Group Milestone', project: project3)
+        create(:active_milestone, title: 'Active Group Milestone', project: project1)
+        create(:active_milestone, title: 'Active Group Milestone', project: project2)
+        create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
+        create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
+        create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
+      end
+
+      it 'returns the quantity of global milestones in each possible state' do
+        expected_count = { opened: 1, closed: 2, all: 2 }
+
+        count = GlobalMilestone.states_count(Project.all)
+
+        expect(count).to eq(expected_count)
+      end
+    end
+
+    context 'when the projects do not have milestones' do
+      before do
+        project1
+      end
+
+      it 'returns 0 as the quantity of global milestones in each state' do
+        expected_count = { opened: 0, closed: 0, all: 0 }
+
+        count = GlobalMilestone.states_count(Project.all)
+
+        expect(count).to eq(expected_count)
+      end
+    end
+  end
+
   describe '#initialize' do
     let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
     let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
@@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do
       expect(global_milestone.safe_title).to eq('git-test')
     end
   end
+
+  describe '#state' do
+    context 'when at least one milestone is active' do
+      it 'returns active' do
+        title = 'Active Group Milestone'
+        milestones = [
+          create(:active_milestone, title: title),
+          create(:closed_milestone, title: title)
+        ]
+        global_milestone = GlobalMilestone.new(title, milestones)
+
+        expect(global_milestone.state).to eq('active')
+      end
+    end
+
+    context 'when all milestones are closed' do
+      it 'returns closed' do
+        title = 'Closed Group Milestone'
+        milestones = [
+          create(:closed_milestone, title: title),
+          create(:closed_milestone, title: title)
+        ]
+        global_milestone = GlobalMilestone.new(title, milestones)
+
+        expect(global_milestone.state).to eq('closed')
+      end
+    end
+  end
 end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index a4e6eb4e3a6e3f0181a1263b73bfbba47b638aa8..5d87938235a67ba63d9a053c8c3d4324439a391e 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -13,6 +13,8 @@ describe Group, models: true do
     it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
     it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
     it { is_expected.to have_many(:labels).class_name('GroupLabel') }
+    it { is_expected.to have_many(:uploads).dependent(:destroy) }
+    it { is_expected.to have_one(:chat_team) }
 
     describe '#members & #requesters' do
       let(:requester) { create(:user) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bba9058f3941e7326c528a9d4148555a1eee665c..73977d031f9d803fb5dc12b5c4574a475e7c3a6b 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,6 +22,45 @@ describe Issue, models: true do
     it { is_expected.to have_db_index(:deleted_at) }
   end
 
+  describe '#order_by_position_and_priority' do
+    let(:project) { create :empty_project }
+    let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+    let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+    let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
+    let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+    let!(:issue3) { create(:issue, project: project, relative_position: 100) }
+    let!(:issue4) { create(:issue, project: project, relative_position: 200) }
+
+    it 'returns ordered list' do
+      expect(project.issues.order_by_position_and_priority).
+        to match [issue3, issue4, issue1, issue2]
+    end
+  end
+
+  describe '#closed_at' do
+    after do
+      Timecop.return
+    end
+
+    let!(:now) { Timecop.freeze(Time.now) }
+
+    it 'sets closed_at to Time.now when issue is closed' do
+      issue = create(:issue, state: 'opened')
+
+      issue.close
+
+      expect(issue.closed_at).to eq(now)
+    end
+
+    it 'sets closed_at to nil when issue is reopened' do
+      issue = create(:issue, state: 'closed')
+
+      issue.reopen
+
+      expect(issue.closed_at).to be_nil
+    end
+  end
+
   describe '#to_reference' do
     let(:namespace) { build(:namespace, path: 'sample-namespace') }
     let(:project)   { build(:empty_project, name: 'sample-project', namespace: namespace) }
@@ -620,4 +659,15 @@ describe Issue, models: true do
       end
     end
   end
+
+  describe '#hook_attrs' do
+    let(:attrs_hash) { subject.hook_attrs }
+
+    it 'includes time tracking attrs' do
+      expect(attrs_hash).to include(:total_time_spent)
+      expect(attrs_hash).to include(:human_time_estimate)
+      expect(attrs_hash).to include(:human_total_time_spent)
+      expect(attrs_hash).to include('time_estimate')
+    end
+  end
 end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index e4be0aba7a6a3711947cd41a4b94708e4f415acb..87ea2e706807d39431b2d90ecc8c4129feb43bcf 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -89,8 +89,8 @@ describe ProjectMember, models: true do
       @user_1 = create :user
       @user_2 = create :user
 
-      @project_1.team << [ @user_1, :developer ]
-      @project_2.team << [ @user_2, :reporter ]
+      @project_1.team << [@user_1, :developer]
+      @project_2.team << [@user_2, :reporter]
 
       @status = @project_2.team.import(@project_1)
     end
@@ -137,8 +137,8 @@ describe ProjectMember, models: true do
       @user_1 = create :user
       @user_2 = create :user
 
-      @project_1.team << [ @user_1, :developer]
-      @project_2.team << [ @user_2, :reporter]
+      @project_1.team << [@user_1, :developer]
+      @project_2.team << [@user_2, :reporter]
 
       ProjectMember.truncate_teams([@project_1.id, @project_2.id])
     end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 6d599e148a258eb8dd415bb58ae18fa75474cb8f..0a10ee015062f638fc9021026fbd22fea8405515 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -109,7 +109,7 @@ describe MergeRequestDiff, models: true do
         { id: 'sha2' }
       ]
 
-      expect(subject.commits_sha).to eq(['sha1', 'sha2'])
+      expect(subject.commits_sha).to eq(%w(sha1 sha2))
     end
   end
 
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a01741a9971348eba0e8ca834b9791553c213d75..24e7c1b17d95291b9c79dbbd8f636466eecbec72 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -37,12 +37,12 @@ describe MergeRequest, models: true do
       end
 
       it "is invalid without merge user" do
-        subject.merge_when_build_succeeds = true
+        subject.merge_when_pipeline_succeeds = true
         expect(subject).not_to be_valid
       end
 
       it "is valid with merge user" do
-        subject.merge_when_build_succeeds = true
+        subject.merge_when_pipeline_succeeds = true
         subject.merge_user = build(:user)
 
         expect(subject).to be_valid
@@ -55,7 +55,7 @@ describe MergeRequest, models: true do
     it { is_expected.to respond_to(:can_be_merged?) }
     it { is_expected.to respond_to(:cannot_be_merged?) }
     it { is_expected.to respond_to(:merge_params) }
-    it { is_expected.to respond_to(:merge_when_build_succeeds) }
+    it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
   end
 
   describe '.in_projects' do
@@ -209,6 +209,50 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#diff_size' do
+    let(:merge_request) do
+      build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
+    end
+
+    context 'when there are MR diffs' do
+      before do
+        merge_request.save
+      end
+
+      it 'returns the correct count' do
+        expect(merge_request.diff_size).to eq(105)
+      end
+
+      it 'does not perform highlighting' do
+        expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+        merge_request.diff_size
+      end
+    end
+
+    context 'when there are no MR diffs' do
+      before do
+        merge_request.compare = CompareService.new(
+          merge_request.source_project,
+          merge_request.source_branch
+        ).execute(
+          merge_request.target_project,
+          merge_request.target_branch
+        )
+      end
+
+      it 'returns the correct count' do
+        expect(merge_request.diff_size).to eq(105)
+      end
+
+      it 'does not perform highlighting' do
+        expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+        merge_request.diff_size
+      end
+    end
+  end
+
   describe "#related_notes" do
     let!(:merge_request) { create(:merge_request) }
 
@@ -302,6 +346,23 @@ describe MergeRequest, models: true do
 
       expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
     end
+
+    context 'when the project has an external issue tracker' do
+      before do
+        subject.project.team << [subject.author, :developer]
+        commit = double(:commit, safe_message: 'Fixes TEST-3')
+
+        create(:jira_service, project: subject.project)
+
+        allow(subject).to receive(:commits).and_return([commit])
+        allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
+        allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
+      end
+
+      it 'detects issues mentioned in description but not closed' do
+        expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
+      end
+    end
   end
 
   describe "#work_in_progress?" do
@@ -464,24 +525,24 @@ describe MergeRequest, models: true do
     end
   end
 
-  describe "#reset_merge_when_build_succeeds" do
+  describe "#reset_merge_when_pipeline_succeeds" do
     let(:merge_if_green) do
-      create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
+      create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
                              merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
     end
 
     it "sets the item to false" do
-      merge_if_green.reset_merge_when_build_succeeds
+      merge_if_green.reset_merge_when_pipeline_succeeds
       merge_if_green.reload
 
-      expect(merge_if_green.merge_when_build_succeeds).to be_falsey
+      expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
       expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
       expect(merge_if_green.merge_params["commit_message"]).to be_nil
     end
   end
 
   describe "#hook_attrs" do
-    let(:attrs_hash) { subject.hook_attrs.to_h }
+    let(:attrs_hash) { subject.hook_attrs }
 
     [:source, :target].each do |key|
       describe "#{key} key" do
@@ -497,6 +558,10 @@ describe MergeRequest, models: true do
       expect(attrs_hash).to include(:target)
       expect(attrs_hash).to include(:last_commit)
       expect(attrs_hash).to include(:work_in_progress)
+      expect(attrs_hash).to include(:total_time_spent)
+      expect(attrs_hash).to include(:human_time_estimate)
+      expect(attrs_hash).to include(:human_total_time_spent)
+      expect(attrs_hash).to include('time_estimate')
     end
   end
 
@@ -768,7 +833,7 @@ describe MergeRequest, models: true do
   end
 
   describe '#check_if_can_be_merged' do
-    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+    let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) }
 
     subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
 
@@ -789,12 +854,6 @@ describe MergeRequest, models: true do
       it 'becomes unmergeable' do
         expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
       end
-
-      it 'creates Todo on unmergeability' do
-        expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject)
-
-        subject.check_if_can_be_merged
-      end
     end
 
     context 'when it has conflicts' do
@@ -889,7 +948,7 @@ describe MergeRequest, models: true do
   end
 
   describe '#mergeable_ci_state?' do
-    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+    let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) }
     let(:pipeline) { create(:ci_empty_pipeline) }
 
     subject { build(:merge_request, target_project: project) }
@@ -932,7 +991,7 @@ describe MergeRequest, models: true do
     end
 
     context 'when merges are not restricted to green builds' do
-      subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }
+      subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_pipeline_succeeds: false)) }
 
       context 'and a failed pipeline is associated' do
         before do
@@ -1537,7 +1596,7 @@ describe MergeRequest, models: true do
         status:  status)
     end
 
-    let(:project)       { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) }
+    let(:project)       { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
     let(:developer)     { create(:user) }
     let(:user)          { create(:user) }
     let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index aeb4eeb0b55d88ffd3951bbbc1f39f21b69fb2f2..d22447c602f2c2f7f1cdc2dad1d3edb8dbb010dc 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,20 @@ describe Namespace, models: true do
       expect(nested).not_to be_valid
       expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting')
     end
+
+    describe 'reserved path validation' do
+      context 'nested group' do
+        let(:group) { build(:group, :nested, path: 'tree') }
+
+        it { expect(group).not_to be_valid }
+      end
+
+      context 'top-level group' do
+        let(:group) { build(:group, path: 'tree') }
+
+        it { expect(group).to be_valid }
+      end
+    end
   end
 
   describe "Respond to" do
@@ -36,7 +50,7 @@ describe Namespace, models: true do
   end
 
   describe '#to_param' do
-    it { expect(namespace.to_param).to eq(namespace.path) }
+    it { expect(namespace.to_param).to eq(namespace.full_path) }
   end
 
   describe '#human_name' do
@@ -153,7 +167,7 @@ describe Namespace, models: true do
 
   describe :rm_dir do
     let!(:project) { create(:empty_project, namespace: namespace) }
-    let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
+    let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
 
     it "removes its dirs when deleted" do
       namespace.destroy
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1cde9e049518f2dc1e614d4f872a62c096b6eb23..33536487c4174f1bf6f2635217832f52e6500246 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -387,4 +387,16 @@ describe Note, models: true do
       end
     end
   end
+
+  describe 'expiring ETag cache' do
+    let(:note) { build(:note_on_issue) }
+
+    it "expires cache for note's issue when note is saved" do
+      expect_any_instance_of(Gitlab::EtagCaching::Store)
+        .to receive(:touch)
+        .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+
+      note.save!
+    end
+  end
 end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 46eb71cef145bddde53ab631c4b6ad242e4062dc..823623d96faaea8068f17cf8eb145d3541063c0c 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -1,15 +1,61 @@
 require 'spec_helper'
 
 describe PersonalAccessToken, models: true do
-  describe ".generate" do
-    it "generates a random token" do
-      personal_access_token = PersonalAccessToken.generate({})
-      expect(personal_access_token.token).to be_present
+  describe '.build' do
+    let(:personal_access_token) { build(:personal_access_token) }
+    let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) }
+
+    it 'is a valid personal access token' do
+      expect(personal_access_token).to be_valid
+    end
+
+    it 'ensures that the token is generated' do
+      invalid_personal_access_token.save!
+
+      expect(invalid_personal_access_token).to be_valid
+      expect(invalid_personal_access_token.token).not_to be_nil
     end
+  end
+
+  describe ".active?" do
+    let(:active_personal_access_token) { build(:personal_access_token) }
+    let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) }
+    let(:expired_personal_access_token) { build(:personal_access_token, :expired) }
+
+    it "returns false if the personal_access_token is revoked" do
+      expect(revoked_personal_access_token).not_to be_active
+    end
+
+    it "returns false if the personal_access_token is expired" do
+      expect(expired_personal_access_token).not_to be_active
+    end
+
+    it "returns true if the personal_access_token is not revoked and not expired" do
+      expect(active_personal_access_token).to be_active
+    end
+  end
+
+  context "validations" do
+    let(:personal_access_token) { build(:personal_access_token) }
+
+    it "requires at least one scope" do
+      personal_access_token.scopes = []
+
+      expect(personal_access_token).not_to be_valid
+      expect(personal_access_token.errors[:scopes].first).to eq "can't be blank"
+    end
+
+    it "allows creating a token with API scopes" do
+      personal_access_token.scopes = [:api, :read_user]
+
+      expect(personal_access_token).to be_valid
+    end
+
+    it "rejects creating a token with non-API scopes" do
+      personal_access_token.scopes = [:openid, :api]
 
-    it "doesn't save the record" do
-      personal_access_token = PersonalAccessToken.generate({})
-      expect(personal_access_token).not_to be_persisted
+      expect(personal_access_token).not_to be_valid
+      expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
     end
   end
 end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 59a4ae1b79900894bf9a7a49f69427eef141e3c6..9b711bfc007501e0c7ec34e14716f567fb1aa595 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -7,12 +7,27 @@ describe ProjectGroupLink do
   end
 
   describe "Validation" do
-    let!(:project_group_link) { create(:project_group_link) }
+    let(:parent_group) { create(:group) }
+    let(:group) { create(:group, parent: parent_group) }
+    let(:project) { create(:project, group: group) }
+    let!(:project_group_link) { create(:project_group_link, project: project) }
 
     it { should validate_presence_of(:project_id) }
     it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
     it { should validate_presence_of(:group) }
     it { should validate_presence_of(:group_access) }
+
+    it "doesn't allow a project to be shared with the group it is in" do
+      project_group_link.group = group
+
+      expect(project_group_link).not_to be_valid
+    end
+
+    it "doesn't allow a project to be shared with an ancestor of the group it is in" do
+      project_group_link.group = parent_group
+
+      expect(project_group_link).not_to be_valid
+    end
   end
 
   describe "destroying a record", truncate: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 497a626a418c8e2bc9ba635bca6cecc705360844..4014d6129eeeb0d7edf1daca67163c61272dfec7 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -181,7 +181,7 @@ describe BambooService, models: true, caching: true do
       end
 
       it 'sets commit status to "pending" when response has no results' do
-        stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+        stub_request(body: %q({"results":{"results":{"size":"0"}}}))
 
         is_expected.to eq('pending')
       end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index dbd23ff54918dd654f47a3601b594cb627193fff..05b602d810655b0a88802d290763c4501eccbb68 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -92,7 +92,7 @@ describe BuildkiteService, models: true, caching: true do
         end
 
         it 'passes through build status untouched when status is 200' do
-          stub_request(body: %Q({"status":"Great Success"}))
+          stub_request(body: %q({"status":"Great Success"}))
 
           is_expected.to eq('Great Success')
         end
@@ -101,7 +101,7 @@ describe BuildkiteService, models: true, caching: true do
   end
 
   def stub_request(status: 200, body: nil)
-    body ||= %Q({"status":"success"})
+    body ||= %q({"status":"success"})
     buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
 
     WebMock.stub_request(:get, buildkite_full_url).to_return(
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e256306bf63ae140b48029eea97259eccd..0000000000000000000000000000000000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
-  let(:data) do
-    Gitlab::DataBuilder::Build.build(create(:ci_build))
-  end
-
-  describe 'Validations' do
-    context 'when service is active' do
-      before { subject.active = true }
-
-      it { is_expected.to validate_presence_of(:recipients) }
-
-      context 'when pusher is added' do
-        before { subject.add_pusher = true }
-
-        it { is_expected.not_to validate_presence_of(:recipients) }
-      end
-    end
-
-    context 'when service is inactive' do
-      before { subject.active = false }
-
-      it { is_expected.not_to validate_presence_of(:recipients) }
-    end
-  end
-
-  describe '#test_data' do
-    let(:build)   { create(:ci_build) }
-    let(:project) { build.project }
-    let(:user)    { create(:user) }
-
-    before { project.team << [user, :developer] }
-
-    it 'builds test data' do
-      data = subject.test_data(project)
-
-      expect(data[:object_kind]).to eq("build")
-    end
-  end
-
-  describe '#test' do
-    it 'sends email' do
-      data = Gitlab::DataBuilder::Build.build(create(:ci_build))
-      subject.recipients = 'test@gitlab.com'
-
-      expect(BuildEmailWorker).to receive(:perform_async)
-
-      subject.test(data)
-    end
-
-    context 'notify only failed builds is true' do
-      it 'sends email' do
-        data = Gitlab::DataBuilder::Build.build(create(:ci_build))
-        data[:build_status] = "success"
-        subject.recipients = 'test@gitlab.com'
-
-        expect(subject).not_to receive(:notify_only_broken_builds)
-        expect(BuildEmailWorker).to receive(:perform_async)
-
-        subject.test(data)
-      end
-    end
-  end
-
-  describe '#execute' do
-    it 'sends email' do
-      subject.recipients = 'test@gitlab.com'
-      data[:build_status] = 'failed'
-
-      expect(BuildEmailWorker).to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with succeeded build and notify_only_broken_builds on' do
-      expect(subject).to receive(:notify_only_broken_builds).and_return(true)
-      data[:build_status] = 'success'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with failed build and build_allow_failure is true' do
-      data[:build_status] = 'failed'
-      data[:build_allow_failure] = true
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with unknown build status' do
-      data[:build_status] = 'foo'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email when recipients list is empty' do
-      subject.recipients = ' ,, '
-      data[:build_status] = 'failed'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-  end
-end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0b8fa70995618e8d9544eda585c261..0000000000000000000000000000000000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
-  subject { described_class.new(args) }
-
-  let(:args) do
-    {
-      sha: '97de212e80737a608d939f648d959671fb0a0142',
-      ref: 'develop',
-      tag: false,
-
-      project_name: 'project_name',
-      project_url: 'http://example.gitlab.com',
-      build_id: 1,
-      build_name: build_name,
-      build_stage: stage,
-
-      commit: {
-        status: status,
-        author_name: 'hacker',
-        author_url: 'http://example.gitlab.com/hacker',
-        duration: duration,
-      },
-    }
-  end
-
-  let(:message) { build_message }
-  let(:stage) { 'test' }
-  let(:status) { 'success' }
-  let(:build_name) { 'rspec' }
-  let(:duration) { 10 }
-
-  context 'build succeeded' do
-    let(:status) { 'success' }
-    let(:color) { 'good' }
-    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])
-    end
-  end
-
-  context 'build failed' do
-    let(:status) { 'failed' }
-    let(:color) { 'danger' }
-
-    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])
-    end
-  end
-
-  it 'returns a message with information on build' do
-    expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
-  end
-
-  it 'returns a message with stage name' do
-    expect(subject.fallback).to include("of stage #{stage}")
-  end
-
-  it 'returns a message with link to author' do
-    expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
-  end
-
-  def build_message(status_text = status, stage_text = stage, build_text = build_name)
-    "<http://example.gitlab.com|project_name>:" \
-    " Commit <http://example.gitlab.com/commit/" \
-    "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
-    " of <http://example.gitlab.com/commits/develop|develop> branch" \
-    " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
-    " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
-    " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
-  end
-end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index f9307d6de7b11181e9a7a2dbbb6edb57c633f47a..044737c6026dd73801068f4ca01ce26f90b84ed7 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -28,7 +28,7 @@ describe DroneCiService, models: true, caching: true do
   shared_context :drone_ci_service do
     let(:drone)      { DroneCiService.new }
     let(:project)    { create(:project, :repository, name: 'project') }
-    let(:path)       { "#{project.namespace.path}/#{project.path}" }
+    let(:path)       { project.full_path }
     let(:drone_url)  { 'http://drone.example.com' }
     let(:sha)        { '2ab7834c' }
     let(:branch)     { 'dev' }
@@ -50,7 +50,7 @@ describe DroneCiService, models: true, caching: true do
     end
 
     def stub_request(status: 200, body: nil)
-      body ||= %Q({"status":"success"})
+      body ||= %q({"status":"success"})
 
       WebMock.stub_request(:get, commit_status_path).to_return(
         status: status,
@@ -95,12 +95,12 @@ describe DroneCiService, models: true, caching: true do
         is_expected.to eq(:error)
       end
 
-      { "killed"  => :canceled,
+      {
+        "killed"  => :canceled,
         "failure" => :failed,
         "error"   => :failed,
-        "success" => "success",
+        "success" => "success"
       }.each do |drone_status, our_status|
-
         it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
           stub_request(body: %Q({"status":"#{drone_status}"}))
 
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1a82c076b38a813be337cba3ed30e3..1200ae7eb224a94303f665899a47f2dbf9487ae3 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -280,13 +280,14 @@ describe HipchatService, models: true do
       end
     end
 
-    context 'build events' do
-      let(:pipeline) { create(:ci_empty_pipeline) }
-      let(:build) { create(:ci_build, pipeline: pipeline) }
-      let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+    context 'pipeline events' do
+      let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+      let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
 
       context 'for failed' do
-        before { build.drop }
+        before do
+          pipeline.drop
+        end
 
         it "calls Hipchat API" do
           hipchat.execute(data)
@@ -295,35 +296,36 @@ describe HipchatService, models: true do
         end
 
         it "creates a build message" do
-          message = hipchat.send(:create_build_message, data)
+          message = hipchat.__send__(:create_pipeline_message, data)
 
           project_url = project.web_url
           project_name = project.name_with_namespace.gsub(/\s/, '')
-          sha = data[:sha]
-          ref = data[:ref]
-          ref_type = data[:tag] ? 'tag' : 'branch'
-          duration = data[:commit][:duration]
+          pipeline_attributes = data[:object_attributes]
+          ref = pipeline_attributes[:ref]
+          ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+          duration = pipeline_attributes[:duration]
+          user_name = data[:user][:name]
 
           expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
-            "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+            "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
             "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
-            "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+            "by #{user_name} failed in #{duration} second(s)")
         end
       end
 
       context 'for succeeded' do
         before do
-          build.success
+          pipeline.succeed
         end
 
         it "calls Hipchat API" do
-          hipchat.notify_only_broken_builds = false
+          hipchat.notify_only_broken_pipelines = false
           hipchat.execute(data)
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
         it "notifies only broken" do
-          hipchat.notify_only_broken_builds = true
+          hipchat.notify_only_broken_pipelines = true
           hipchat.execute(data)
           expect(WebMock).not_to have_requested(:post, api_url).once
         end
@@ -349,17 +351,19 @@ describe HipchatService, models: true do
 
       context 'with a successful build' do
         it 'uses the green color' do
-          build_data = { object_kind: 'build', commit: { status: 'success' } }
+          data = { object_kind: 'pipeline',
+                   object_attributes: { status: 'success' } }
 
-          expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+          expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
         end
       end
 
       context 'with a failed build' do
         it 'uses the red color' do
-          build_data = { object_kind: 'build', commit: { status: 'failed' } }
+          data = { object_kind: 'pipeline',
+                   object_attributes: { status: 'failed' } }
 
-          expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+          expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
         end
       end
     end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index b9fb6f3f6f487ac0a47e0bbcb2ea15744c5fdc46..d5a16226d9d1de7becd2cae0a9f1baf84beddc12 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -59,8 +59,8 @@ describe IrkerService, models: true do
 
       conn = @irker_server.accept
       conn.readlines.each do |line|
-        msg = JSON.load(line.chomp("\n"))
-        expect(msg.keys).to match_array(['to', 'privmsg'])
+        msg = JSON.parse(line.chomp("\n"))
+        expect(msg.keys).to match_array(%w(to privmsg))
         expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
                                           "irc://test.net/#test"])
       end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fbe6f344a98d1cb99831578f9a2019f3206fc4b1
--- /dev/null
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe IssueTrackerService, models: true do
+  describe 'Validations' do
+    let(:project) { create :project }
+
+    describe 'only one issue tracker per project' do
+      let(:service) { RedmineService.new(project: project, active: true) }
+
+      before do
+        create(:service, project: project, active: true, category: 'issue_tracker')  
+      end
+
+      context 'when service is changed manually by user' do
+        it 'executes the validation' do
+          valid = service.valid?(:manual_change)
+
+          expect(valid).to be_falsey
+          expect(service.errors[:base]).to include(
+            'Another issue tracker is already in use. Only one issue tracker service can be active at a time'
+          )
+        end
+      end
+
+      context 'when service is changed internally' do
+        it 'does not execute the validation' do
+          expect(service.valid?).to be_truthy
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 9052479d35eb48916b71783b9dccd20445d05069..bf7950ef1c955c0a11c71dd921ccb94941d67c56 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -74,8 +74,10 @@ describe KubernetesService, models: true, caching: true do
 
   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)
+      let(:namespace_name) { "#{project.path}-#{project.id}" }
+
+      it 'defaults to the project name with ID' do
+        expect(described_class.new(project: project).namespace).to eq(namespace_name)
       end
     end
 
@@ -163,6 +165,12 @@ describe KubernetesService, models: true, caching: true do
         { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
       )
     end
+
+    it 'sets KUBE_CA_PEM_FILE' do
+      expect(subject.predefined_variables).to include(
+        { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
+      )
+    end
   end
 
   describe '#terminals' do
@@ -171,7 +179,7 @@ describe KubernetesService, models: true, caching: true do
 
     context 'with invalid pods' do
       it 'returns no terminals' do
-        stub_reactive_cache(service, pods: [ { "bad" => "pod" } ])
+        stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
 
         is_expected.to be_empty
       end
@@ -184,7 +192,7 @@ describe KubernetesService, models: true, caching: true do
       before do
         stub_reactive_cache(
           service,
-          pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]
+          pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
         )
       end
 
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 98f3d420c8a29b31a3ad626b7f65fb2e34e5d745..f9531be5d251e8e32e9bd9af2ddce15415b2ac35 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -36,7 +36,8 @@ describe MattermostSlashCommandsService, :models do
               description: "Perform common operations on: #{project.name_with_namespace}",
               display_name: "GitLab / #{project.name_with_namespace}",
               method: 'P',
-              username: 'GitLab' }.to_json).
+              username: 'GitLab'
+            }.to_json).
             to_return(
               status: 200,
               headers: { 'Content-Type' => 'application/json' },
@@ -91,7 +92,7 @@ describe MattermostSlashCommandsService, :models do
             to_return(
               status: 200,
               headers: { 'Content-Type' => 'application/json' },
-              body: ['list'].to_json
+              body: { 'list' => true }.to_json
             )
         end
 
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d15079b686be95569f70ebe388816cdf48429ecd
--- /dev/null
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe PrometheusService, models: true, caching: true do
+  include PrometheusHelpers
+  include ReactiveCachingHelpers
+
+  let(:project) { create(:prometheus_project) }
+  let(:service) { project.prometheus_service }
+
+  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(:api_url) }
+    end
+
+    context 'when service is inactive' do
+      before { subject.active = false }
+
+      it { is_expected.not_to validate_presence_of(:api_url) }
+    end
+  end
+
+  describe '#test' do
+    let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) }
+
+    context 'success' do
+      it 'reads the discovery endpoint' do
+        expect(service.test[:success]).to be_truthy
+        expect(req_stub).to have_been_requested
+      end
+    end
+
+    context 'failure' do
+      let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), status: 404) }
+
+      it 'fails to read the discovery endpoint' do
+        expect(service.test[:success]).to be_falsy
+        expect(req_stub).to have_been_requested
+      end
+    end
+  end
+
+  describe '#metrics' do
+    let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+    subject { service.metrics(environment) }
+
+    around do |example|
+      Timecop.freeze { example.run }
+    end
+
+    context 'with valid data' do
+      before do
+        stub_reactive_cache(service, prometheus_data, 'env-slug')
+      end
+
+      it 'returns reactive data' do
+        is_expected.to eq(prometheus_data)
+      end
+    end
+  end
+
+  describe '#calculate_reactive_cache' do
+    let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+
+    around do |example|
+      Timecop.freeze { example.run }
+    end
+
+    subject do
+      service.calculate_reactive_cache(environment.slug)
+    end
+
+    context 'when service is inactive' do
+      before do
+        service.active = false
+      end
+
+      it { is_expected.to be_nil }
+    end
+
+    context 'when Prometheus responds with valid data' do
+      before do
+        stub_all_prometheus_requests(environment.slug)
+      end
+
+      it { expect(subject.to_json).to eq(prometheus_data.to_json) }
+    end
+
+    [404, 500].each do |status|
+      context "when Prometheus responds with #{status}" do
+        before do
+          stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
+        end
+
+        it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
+      end
+    end
+  end
+end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index a1edd083aa1c456bffdeae8855840eef648c02e4..77b18e1c7d0f0b4ed4d60ce38d325dbb905a2944 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -143,7 +143,7 @@ describe TeamcityService, models: true, caching: true do
       end
 
       it 'returns a build URL when teamcity_url has no trailing slash' do
-        stub_request(body: %Q({"build":{"id":"666"}}))
+        stub_request(body: %q({"build":{"id":"666"}}))
 
         is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
       end
@@ -152,7 +152,7 @@ describe TeamcityService, models: true, caching: true do
         let(:teamcity_url) { 'http://gitlab.com/teamcity/' }
 
         it 'returns a build URL' do
-          stub_request(body: %Q({"build":{"id":"666"}}))
+          stub_request(body: %q({"build":{"id":"666"}}))
 
           is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
         end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 77f2ff3d17b495dbb5a50b93e9b62ad9e77a0cde..aefbedf0b931fb68917a84642a3d60b1921dea60 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,8 +29,7 @@ describe Project, models: true do
     it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
     it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
     it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
-    it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
-    it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+    it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) }
     it { is_expected.to have_one(:irker_service).dependent(:destroy) }
     it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
     it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
@@ -71,6 +70,7 @@ describe Project, models: true do
     it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
     it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
     it { is_expected.to have_many(:forks).through(:forked_project_links) }
+    it { is_expected.to have_many(:uploads).dependent(:destroy) }
 
     context 'after initialized' do
       it "has a project_feature" do
@@ -178,7 +178,7 @@ describe Project, models: true do
       let(:project2) { build(:empty_project, repository_storage: 'missing') }
 
       before do
-        storages = { 'custom' => 'tmp/tests/custom_repositories' }
+        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
         allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
       end
 
@@ -218,6 +218,20 @@ describe Project, models: true do
       expect(project2.import_data).to be_nil
     end
 
+    it "does not allow blocked import_url localhost" do
+      project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git')
+
+      expect(project2).to be_invalid
+      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+    end
+
+    it "does not allow blocked import_url port" do
+      project2 = build(:empty_project, import_url: 'http://github.com:25/t.git')
+
+      expect(project2).to be_invalid
+      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+    end
+
     describe 'project pending deletion' do
       let!(:project_pending_deletion) do
         create(:empty_project,
@@ -380,7 +394,7 @@ describe Project, models: true do
 
     before do
       FileUtils.mkdir('tmp/tests/custom_repositories')
-      storages = { 'custom' => 'tmp/tests/custom_repositories' }
+      storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
     end
 
@@ -402,7 +416,7 @@ describe Project, models: true do
     let(:project) { create(:empty_project, path: "somewhere") }
 
     it 'returns the full web URL for this repo' do
-      expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
+      expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
     end
   end
 
@@ -803,7 +817,7 @@ describe Project, models: true do
       end
 
       let(:avatar_path) do
-        "/#{project.namespace.name}/#{project.path}/avatar"
+        "/#{project.full_path}/avatar"
       end
 
       it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
@@ -946,8 +960,8 @@ describe Project, models: true do
 
     before do
       storages = {
-        'default' => 'tmp/tests/repositories',
-        'picked'  => 'tmp/tests/repositories',
+        'default' => { 'path' => 'tmp/tests/repositories' },
+        'picked'  => { 'path' => 'tmp/tests/repositories' },
       }
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
     end
@@ -1148,16 +1162,14 @@ describe Project, models: true do
     end
 
     it 'renames a repository' do
-      ns = project.namespace_dir
-
       expect(gitlab_shell).to receive(:mv_repository).
         ordered.
-        with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}").
+        with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
         and_return(true)
 
       expect(gitlab_shell).to receive(:mv_repository).
         ordered.
-        with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
+        with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki").
         and_return(true)
 
       expect_any_instance_of(SystemHooksService).
@@ -1166,7 +1178,7 @@ describe Project, models: true do
 
       expect_any_instance_of(Gitlab::UploadsTransfer).
         to receive(:rename_project).
-        with('foo', project.path, ns)
+        with('foo', project.path, project.namespace.full_path)
 
       expect(project).to receive(:expire_caches_before_rename)
 
@@ -1513,7 +1525,7 @@ describe Project, models: true do
       it 'schedules a RepositoryForkWorker job' do
         expect(RepositoryForkWorker).to receive(:perform_async).
           with(project.id, forked_from_project.repository_storage_path,
-              forked_from_project.path_with_namespace, project.namespace.path)
+              forked_from_project.path_with_namespace, project.namespace.full_path)
 
         project.add_import_job
       end
@@ -1727,7 +1739,7 @@ describe Project, models: true do
   describe 'inside_path' do
     let!(:project1) { create(:empty_project) }
     let!(:project2) { create(:empty_project) }
-    let!(:path) { project1.namespace.path }
+    let!(:path) { project1.namespace.full_path }
 
     it { expect(Project.inside_path(path)).to eq([project1]) }
   end
@@ -1742,7 +1754,7 @@ describe Project, models: true do
     end
 
     before do
-      project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+      project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master')
     end
 
     context 'when there is a .gitlab/route-map.yml at the commit' do
@@ -1871,4 +1883,34 @@ describe Project, models: true do
       end
     end
   end
+
+  describe '#http_url_to_repo' do
+    let(:project) { create :empty_project }
+
+    context 'when no user is given' do
+      it 'returns the url to the repo without a username' do
+        expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+        expect(project.http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when user is given' do
+      it 'returns the url to the repo with the username' do
+        user = build_stubbed(:user)
+
+        expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+      end
+    end
+  end
+
+  describe '#pipeline_status' do
+    let(:project) { create(:project) }
+    it 'builds a pipeline status' do
+      expect(project.pipeline_status).to be_a(Ci::PipelineStatus)
+    end
+
+    it 'hase a loaded pipeline status' do
+      expect(project.pipeline_status).to be_loaded
+    end
+  end
 end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 58b57bd4fef4dc94d1eecd60f2d6f4ae33f6c5c3..b5b9cd024b0568e251543ef20d26204c9ade61ca 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -35,10 +35,23 @@ describe ProjectWiki, models: true do
   end
 
   describe "#http_url_to_repo" do
-    it "provides the full http url to the repo" do
-      gitlab_url = Gitlab.config.gitlab.url
-      repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git"
-      expect(subject.http_url_to_repo).to eq(repo_http_url)
+    let(:project) { create :empty_project }
+
+    context 'when no user is given' do
+      it 'returns the url to the repo without a username' do
+        expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git"
+
+        expect(project_wiki.http_url_to_repo).to eq(expected_url)
+        expect(project_wiki.http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when user is given' do
+      it 'returns the url to the repo with the username' do
+        user = build_stubbed(:user)
+
+        expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+      end
     end
   end
 
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 838fd3754b202749bfdfd36745a7bb64ddc39f56..274e4f00a0ae7095e45e2bbf3dd033bfcb93ade9 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -291,10 +291,10 @@ describe Repository, models: true do
     end
   end
 
-  describe "#commit_dir" do
+  describe "#create_dir" do
     it "commits a change that creates a new directory" do
       expect do
-        repository.commit_dir(user, 'newdir',
+        repository.create_dir(user, 'newdir',
           message: 'Create newdir', branch_name: 'master')
       end.to change { repository.commits('master').count }.by(1)
 
@@ -307,7 +307,7 @@ describe Repository, models: true do
 
       it "creates a fork and commit to the forked project" do
         expect do
-          repository.commit_dir(user, 'newdir',
+          repository.create_dir(user, 'newdir',
             message: 'Create newdir', branch_name: 'patch',
             start_branch_name: 'master', start_project: forked_project)
         end.to change { repository.commits('master').count }.by(0)
@@ -323,7 +323,7 @@ describe Repository, models: true do
     context "when an author is specified" do
       it "uses the given email/name to set the commit's author" do
         expect do
-          repository.commit_dir(user, 'newdir',
+          repository.create_dir(user, 'newdir',
             message: 'Add newdir',
             branch_name: 'master',
             author_email: author_email, author_name: author_name)
@@ -337,25 +337,23 @@ describe Repository, models: true do
     end
   end
 
-  describe "#commit_file" do
-    it 'commits change to a file successfully' do
+  describe "#create_file" do
+    it 'commits new file successfully' do
       expect do
-        repository.commit_file(user, 'CHANGELOG', 'Changelog!',
-                               message: 'Updates file content',
-                               branch_name: 'master',
-                               update: true)
+        repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
+                               message: 'Create changelog',
+                               branch_name: 'master')
       end.to change { repository.commits('master').count }.by(1)
 
-      blob = repository.blob_at('master', 'CHANGELOG')
+      blob = repository.blob_at('master', 'NEWCHANGELOG')
 
       expect(blob.data).to eq('Changelog!')
     end
 
     it 'respects the autocrlf setting' do
-      repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld",
+      repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
                              message: 'Add hello world',
-                             branch_name: 'master',
-                             update: true)
+                             branch_name: 'master')
 
       blob = repository.blob_at('master', 'hello.txt')
 
@@ -365,10 +363,9 @@ describe Repository, models: true do
     context "when an author is specified" do
       it "uses the given email/name to set the commit's author" do
         expect do
-          repository.commit_file(user, 'README', 'README!',
+          repository.create_file(user, 'NEWREADME', 'README!',
                                  message: 'Add README',
                                  branch_name: 'master',
-                                 update: true,
                                  author_email: author_email,
                                  author_name: author_name)
         end.to change { repository.commits('master').count }.by(1)
@@ -382,6 +379,18 @@ describe Repository, models: true do
   end
 
   describe "#update_file" do
+    it 'updates file successfully' do
+      expect do
+        repository.update_file(user, 'CHANGELOG', 'Changelog!',
+                               message: 'Update changelog',
+                               branch_name: 'master')
+      end.to change { repository.commits('master').count }.by(1)
+
+      blob = repository.blob_at('master', 'CHANGELOG')
+
+      expect(blob.data).to eq('Changelog!')
+    end
+
     it 'updates filename successfully' do
       expect do
         repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -398,9 +407,6 @@ describe Repository, models: true do
 
     context "when an author is specified" do
       it "uses the given email/name to set the commit's author" do
-        repository.commit_file(user, 'README', 'README!',
-          message: 'Add README', branch_name: 'master', update: true)
-
         expect do
           repository.update_file(user, 'README', 'Updated README!',
                                  branch_name: 'master',
@@ -418,13 +424,10 @@ describe Repository, models: true do
     end
   end
 
-  describe "#remove_file" do
+  describe "#delete_file" do
     it 'removes file successfully' do
-      repository.commit_file(user, 'README', 'README!',
-        message: 'Add README', branch_name: 'master', update: true)
-
       expect do
-        repository.remove_file(user, 'README',
+        repository.delete_file(user, 'README',
           message: 'Remove README', branch_name: 'master')
       end.to change { repository.commits('master').count }.by(1)
 
@@ -433,11 +436,8 @@ describe Repository, models: true do
 
     context "when an author is specified" do
       it "uses the given email/name to set the commit's author" do
-        repository.commit_file(user, 'README', 'README!',
-          message: 'Add README', branch_name: 'master', update: true)
-
         expect do
-          repository.remove_file(user, 'README',
+          repository.delete_file(user, 'README',
             message: 'Remove README', branch_name: 'master',
             author_email: author_email, author_name: author_name)
         end.to change { repository.commits('master').count }.by(1)
@@ -587,14 +587,14 @@ describe Repository, models: true do
 
   describe "#license_blob", caching: true do
     before do
-      repository.remove_file(
+      repository.delete_file(
         user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
     end
 
     it 'handles when HEAD points to non-existent ref' do
-      repository.commit_file(
+      repository.create_file(
         user, 'LICENSE', 'Copyright!',
-        message: 'Add LICENSE', branch_name: 'master', update: false)
+        message: 'Add LICENSE', branch_name: 'master')
 
       allow(repository).to receive(:file_on_head).
         and_raise(Rugged::ReferenceError)
@@ -603,27 +603,27 @@ describe Repository, models: true do
     end
 
     it 'looks in the root_ref only' do
-      repository.remove_file(user, 'LICENSE',
+      repository.delete_file(user, 'LICENSE',
         message: 'Remove LICENSE', branch_name: 'markdown')
-      repository.commit_file(user, 'LICENSE',
+      repository.create_file(user, 'LICENSE',
         Licensee::License.new('mit').content,
-        message: 'Add LICENSE', branch_name: 'markdown', update: false)
+        message: 'Add LICENSE', branch_name: 'markdown')
 
       expect(repository.license_blob).to be_nil
     end
 
     it 'detects license file with no recognizable open-source license content' do
-      repository.commit_file(user, 'LICENSE', 'Copyright!',
-        message: 'Add LICENSE', branch_name: 'master', update: false)
+      repository.create_file(user, 'LICENSE', 'Copyright!',
+        message: 'Add LICENSE', branch_name: 'master')
 
       expect(repository.license_blob.name).to eq('LICENSE')
     end
 
     %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
       it "detects '#{filename}'" do
-        repository.commit_file(user, filename,
+        repository.create_file(user, filename,
           Licensee::License.new('mit').content,
-          message: "Add #{filename}", branch_name: 'master', update: false)
+          message: "Add #{filename}", branch_name: 'master')
 
         expect(repository.license_blob.name).to eq(filename)
       end
@@ -632,7 +632,7 @@ describe Repository, models: true do
 
   describe '#license_key', caching: true do
     before do
-      repository.remove_file(user, 'LICENSE',
+      repository.delete_file(user, 'LICENSE',
         message: 'Remove LICENSE', branch_name: 'master')
     end
 
@@ -647,16 +647,16 @@ describe Repository, models: true do
     end
 
     it 'detects license file with no recognizable open-source license content' do
-      repository.commit_file(user, 'LICENSE', 'Copyright!',
-        message: 'Add LICENSE', branch_name: 'master', update: false)
+      repository.create_file(user, 'LICENSE', 'Copyright!',
+        message: 'Add LICENSE', branch_name: 'master')
 
       expect(repository.license_key).to be_nil
     end
 
     it 'returns the license key' do
-      repository.commit_file(user, 'LICENSE',
+      repository.create_file(user, 'LICENSE',
         Licensee::License.new('mit').content,
-        message: 'Add LICENSE', branch_name: 'master', update: false)
+        message: 'Add LICENSE', branch_name: 'master')
 
       expect(repository.license_key).to eq('mit')
     end
@@ -913,10 +913,9 @@ describe Repository, models: true do
         expect(empty_repository).to receive(:expire_emptiness_caches)
         expect(empty_repository).to receive(:expire_branches_cache)
 
-        empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+        empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
                                      message: 'Updates file content',
-                                     branch_name: 'master',
-                                     update: false)
+                                     branch_name: 'master')
       end
     end
   end
@@ -1043,7 +1042,7 @@ describe Repository, models: true do
 
     it 'expires the cache for all branches' do
       expect(cache).to receive(:expire).
-        at_least(repository.branches.length).
+        at_least(repository.branches.length * 2).
         times
 
       repository.expire_branch_cache
@@ -1051,14 +1050,14 @@ describe Repository, models: true do
 
     it 'expires the cache for all branches when the root branch is given' do
       expect(cache).to receive(:expire).
-        at_least(repository.branches.length).
+        at_least(repository.branches.length * 2).
         times
 
       repository.expire_branch_cache(repository.root_ref)
     end
 
     it 'expires the cache for a specific branch' do
-      expect(cache).to receive(:expire).once
+      expect(cache).to receive(:expire).twice
 
       repository.expire_branch_cache('foo')
     end
@@ -1113,16 +1112,16 @@ describe Repository, models: true do
     let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
 
     context 'when there is a conflict' do
-      it 'aborts the operation' do
-        expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
+      it 'raises an error' do
+        expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/)
       end
     end
 
     context 'when commit was already reverted' do
-      it 'aborts the operation' do
+      it 'raises an error' do
         repository.revert(user, update_image_commit, 'master')
 
-        expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
+        expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/)
       end
     end
 
@@ -1149,16 +1148,16 @@ describe Repository, models: true do
     let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
 
     context 'when there is a conflict' do
-      it 'aborts the operation' do
-        expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
+      it 'raises an error' do
+        expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/)
       end
     end
 
     context 'when commit was already cherry-picked' do
-      it 'aborts the operation' do
+      it 'raises an error' do
         repository.cherry_pick(user, pickable_commit, 'master')
 
-        expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
+        expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/)
       end
     end
 
@@ -1382,13 +1381,13 @@ describe Repository, models: true do
 
   describe '#branch_count' do
     it 'returns the number of branches' do
-      expect(repository.branch_count).to be_an_instance_of(Fixnum)
+      expect(repository.branch_count).to be_an(Integer)
     end
   end
 
   describe '#tag_count' do
     it 'returns the number of tags' do
-      expect(repository.tag_count).to be_an_instance_of(Fixnum)
+      expect(repository.tag_count).to be_an(Integer)
     end
   end
 
@@ -1738,7 +1737,30 @@ describe Repository, models: true do
 
     context 'with an existing repository' do
       it 'returns the commit count' do
-        expect(repository.commit_count).to be_an_instance_of(Fixnum)
+        expect(repository.commit_count).to be_an(Integer)
+      end
+    end
+  end
+
+  describe '#commit_count_for_ref' do
+    let(:project) { create :empty_project }
+
+    context 'with a non-existing repository' do
+      it 'returns 0' do
+        expect(project.repository.commit_count_for_ref('master')).to eq(0)
+      end
+    end
+
+    context 'with empty repository' do
+      it 'returns 0' do
+        project.create_repository
+        expect(project.repository.commit_count_for_ref('master')).to eq(0)
+      end
+    end
+
+    context 'when searching for the root ref' do
+      it 'returns the same count as #commit_count' do
+        expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count)
       end
     end
   end
@@ -1796,7 +1818,7 @@ describe Repository, models: true do
 
   describe '#gitlab_ci_yml_for' do
     before do
-      repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false)
+      repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
     end
 
     context 'when there is a .gitlab-ci.yml at the commit' do
@@ -1814,7 +1836,7 @@ describe Repository, models: true do
 
   describe '#route_map_for' do
     before do
-      repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+      repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master')
     end
 
     context 'when there is a .gitlab/route-map.yml at the commit' do
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 0b222022e62360aeaf0fcf3b170ee7e7918f55fe..bc8ae4ae5a8531d12e6f7ad36e6d3cbde8e59256 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -43,14 +43,22 @@ describe Route, models: true do
     end
 
     context 'name update' do
-      before { route.update_attributes(name: 'bar') }
-
       it "updates children routes with new path" do
+        route.update_attributes(name: 'bar') 
+
         expect(described_class.exists?(name: 'bar')).to be_truthy
         expect(described_class.exists?(name: 'bar / test')).to be_truthy
         expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
         expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
       end
+
+      it 'handles a rename from nil' do
+        # Note: using `update_columns` to skip all validation and callbacks
+        route.update_columns(name: nil)
+
+        expect { route.update_attributes(name: 'bar') }
+          .to change { route.name }.from(nil).to('bar')
+      end
     end
   end
 end
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4c832c87d6afcbffaaa390db292905c39beb1147
--- /dev/null
+++ b/spec/models/upload_spec.rb
@@ -0,0 +1,151 @@
+require 'rails_helper'
+
+describe Upload, type: :model do
+  describe 'assocations' do
+    it { is_expected.to belong_to(:model) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:size) }
+    it { is_expected.to validate_presence_of(:path) }
+    it { is_expected.to validate_presence_of(:model) }
+    it { is_expected.to validate_presence_of(:uploader) }
+  end
+
+  describe 'callbacks' do
+    context 'for a file above the checksum threshold' do
+      it 'schedules checksum calculation' do
+        stub_const('UploadChecksumWorker', spy)
+
+        upload = described_class.create(
+          path: __FILE__,
+          size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
+          model: build_stubbed(:user),
+          uploader: double('ExampleUploader')
+        )
+
+        expect(UploadChecksumWorker)
+          .to have_received(:perform_async).with(upload.id)
+      end
+    end
+
+    context 'for a file at or below the checksum threshold' do
+      it 'calculates checksum immediately before save' do
+        upload = described_class.new(
+          path: __FILE__,
+          size: described_class::CHECKSUM_THRESHOLD,
+          model: build_stubbed(:user),
+          uploader: double('ExampleUploader')
+        )
+
+        expect { upload.save }
+          .to change { upload.checksum }.from(nil)
+          .to(a_string_matching(/\A\h{64}\z/))
+      end
+    end
+  end
+
+  describe '.remove_path' do
+    it 'removes all records at the given path' do
+      described_class.create!(
+        size: File.size(__FILE__),
+        path: __FILE__,
+        model: build_stubbed(:user),
+        uploader: 'AvatarUploader'
+      )
+
+      expect { described_class.remove_path(__FILE__) }.
+        to change { described_class.count }.from(1).to(0)
+    end
+  end
+
+  describe '.record' do
+    let(:fake_uploader) do
+      double(
+        file: double(size: 12_345),
+        relative_path: 'foo/bar.jpg',
+        model: build_stubbed(:user),
+        class: 'AvatarUploader'
+      )
+    end
+
+    it 'removes existing paths before creation' do
+      expect(described_class).to receive(:remove_path)
+        .with(fake_uploader.relative_path)
+
+      described_class.record(fake_uploader)
+    end
+
+    it 'creates a new record and assigns size, path, model, and uploader' do
+      upload = described_class.record(fake_uploader)
+
+      aggregate_failures do
+        expect(upload).to be_persisted
+        expect(upload.size).to eq fake_uploader.file.size
+        expect(upload.path).to eq fake_uploader.relative_path
+        expect(upload.model_id).to eq fake_uploader.model.id
+        expect(upload.model_type).to eq fake_uploader.model.class.to_s
+        expect(upload.uploader).to eq fake_uploader.class
+      end
+    end
+  end
+
+  describe '#absolute_path' do
+    it 'returns the path directly when already absolute' do
+      path = '/path/to/namespace/project/secret/file.jpg'
+      upload = described_class.new(path: path)
+
+      expect(upload).not_to receive(:uploader_class)
+
+      expect(upload.absolute_path).to eq path
+    end
+
+    it "delegates to the uploader's absolute_path method" do
+      uploader = spy('FakeUploader')
+      upload = described_class.new(path: 'secret/file.jpg')
+      expect(upload).to receive(:uploader_class).and_return(uploader)
+
+      upload.absolute_path
+
+      expect(uploader).to have_received(:absolute_path).with(upload)
+    end
+  end
+
+  describe '#calculate_checksum' do
+    it 'calculates the SHA256 sum' do
+      upload = described_class.new(
+        path: __FILE__,
+        size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+      )
+      expected = Digest::SHA256.file(__FILE__).hexdigest
+
+      expect { upload.calculate_checksum }
+        .to change { upload.checksum }.from(nil).to(expected)
+    end
+
+    it 'returns nil for a non-existant file' do
+      upload = described_class.new(
+        path: __FILE__,
+        size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+      )
+
+      expect(upload).to receive(:exist?).and_return(false)
+
+      expect(upload.calculate_checksum).to be_nil
+    end
+  end
+
+  describe '#exist?' do
+    it 'returns true when the file exists' do
+      upload = described_class.new(path: __FILE__)
+
+      expect(upload).to exist
+    end
+
+    it 'returns false when the file does not exist' do
+      upload = described_class.new(path: "#{__FILE__}-nope")
+
+      expect(upload).not_to exist
+    end
+  end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 584a4facd94a5d0e0098d64a987d56a5134db449..90378179e328ddfff7161fa7cf996754b8fe08f8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,7 +22,7 @@ describe User, models: true do
     it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
     it { is_expected.to have_many(:events).dependent(:destroy) }
     it { is_expected.to have_many(:recent_events).class_name('Event') }
-    it { is_expected.to have_many(:issues).dependent(:destroy) }
+    it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
     it { is_expected.to have_many(:notes).dependent(:destroy) }
     it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
     it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
@@ -32,9 +32,11 @@ describe User, models: true do
     it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
     it { is_expected.to have_many(:todos).dependent(:destroy) }
     it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+    it { is_expected.to have_many(:triggers).dependent(:destroy) }
     it { is_expected.to have_many(:builds).dependent(:nullify) }
     it { is_expected.to have_many(:pipelines).dependent(:nullify) }
     it { is_expected.to have_many(:chat_names).dependent(:destroy) }
+    it { is_expected.to have_many(:uploads).dependent(:destroy) }
 
     describe '#group_members' do
       it 'does not include group memberships for which user is a requester' do
@@ -693,12 +695,13 @@ describe User, models: true do
   end
 
   describe '.search_with_secondary_emails' do
-    def search_with_secondary_emails(query)
-      described_class.search_with_secondary_emails(query)
-    end
+    delegate :search_with_secondary_emails, to: :described_class
 
-    let!(:user) { create(:user) }
-    let!(:email) { create(:email) }
+    let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
+    let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
+    let!(:email) do
+      create(:email, user: another_user, email: 'alias@example.com')
+    end
 
     it 'returns users with a matching name' do
       expect(search_with_secondary_emails(user.name)).to eq([user])
@@ -1415,7 +1418,7 @@ describe User, models: true do
     it { expect(user.nested_groups).to eq([nested_group]) }
   end
 
-  describe '#nested_projects' do
+  describe '#nested_groups_projects' do
     let!(:user) { create(:user) }
     let!(:group) { create(:group) }
     let!(:nested_group) { create(:group, parent: group) }
@@ -1430,7 +1433,7 @@ describe User, models: true do
       other_project.add_developer(create(:user))
     end
 
-    it { expect(user.nested_projects).to eq([nested_project]) }
+    it { expect(user.nested_groups_projects).to eq([nested_project]) }
   end
 
   describe '#refresh_authorized_projects', redis: true do
@@ -1492,4 +1495,41 @@ describe User, models: true do
       expect(user.admin).to be true
     end
   end
+
+  describe '.ghost' do
+    it "creates a ghost user if one isn't already present" do
+      ghost = User.ghost
+
+      expect(ghost).to be_ghost
+      expect(ghost).to be_persisted
+    end
+
+    it "does not create a second ghost user if one is already present" do
+      expect do
+        User.ghost
+        User.ghost
+      end.to change { User.count }.by(1)
+      expect(User.ghost).to eq(User.ghost)
+    end
+
+    context "when a regular user exists with the username 'ghost'" do
+      it "creates a ghost user with a non-conflicting username" do
+        create(:user, username: 'ghost')
+        ghost = User.ghost
+
+        expect(ghost).to be_persisted
+        expect(ghost.username).to eq('ghost1')
+      end
+    end
+
+    context "when a regular user exists with the email 'ghost@example.com'" do
+      it "creates a ghost user with a non-conflicting email" do
+        create(:user, email: 'ghost@example.com')
+        ghost = User.ghost
+
+        expect(ghost).to be_persisted
+        expect(ghost.email).to eq('ghost1@example.com')
+      end
+    end
+  end
 end
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index 63acc0b68cd76097fae0babba672461faa6e549e..02acdcb36df2647756423081e64465a2b3d4c8d7 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,17 +1,19 @@
 require 'spec_helper'
 
 describe BasePolicy, models: true do
-  let(:build) { Ci::Build.new }
-
   describe '.class_for' do
     it 'detects policy class based on the subject ancestors' do
-      expect(described_class.class_for(build)).to eq(Ci::BuildPolicy)
+      expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
     end
 
     it 'detects policy class for a presented subject' do
-      presentee = Ci::BuildPresenter.new(build)
+      presentee = Ci::BuildPresenter.new(Ci::Build.new)
 
       expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy)
     end
+
+    it 'uses GlobalPolicy when :global is given' do
+      expect(described_class.class_for(:global)).to eq(GlobalPolicy)
+    end
   end
 end
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63ad5eb732267ed44d378476df2eb02caf567b6b
--- /dev/null
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Ci::TriggerPolicy, :models do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+  let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+
+  let(:policies) do
+    described_class.abilities(user, trigger).to_set
+  end
+
+  shared_examples 'allows to admin and manage trigger' do
+    it 'does include ability to admin trigger' do
+      expect(policies).to include :admin_trigger
+    end
+
+    it 'does include ability to manage trigger' do
+      expect(policies).to include :manage_trigger
+    end
+  end
+
+  shared_examples 'allows to manage trigger' do
+    it 'does not include ability to admin trigger' do
+      expect(policies).not_to include :admin_trigger
+    end
+
+    it 'does include ability to manage trigger' do
+      expect(policies).to include :manage_trigger
+    end
+  end
+
+  shared_examples 'disallows to admin and manage trigger' do
+    it 'does not include ability to admin trigger' do
+      expect(policies).not_to include :admin_trigger
+    end
+
+    it 'does not include ability to manage trigger' do
+      expect(policies).not_to include :manage_trigger
+    end
+  end
+
+  describe '#rules' do
+    context 'when owner is undefined' do
+      let(:owner) { nil }
+
+      context 'when user is master of the project' do
+        before do
+          project.team << [user, :master]
+        end
+
+        it_behaves_like 'allows to admin and manage trigger'
+      end
+
+      context 'when user is developer of the project' do
+        before do
+          project.team << [user, :developer]
+        end
+
+        it_behaves_like 'disallows to admin and manage trigger'
+      end
+
+      context 'when user is not member of the project' do
+        it_behaves_like 'disallows to admin and manage trigger'
+      end
+    end
+
+    context 'when owner is an user' do
+      let(:owner) { user }
+
+      context 'when user is master of the project' do
+        before do
+          project.team << [user, :master]
+        end
+
+        it_behaves_like 'allows to admin and manage trigger'
+      end
+    end
+
+    context 'when owner is another user' do
+      let(:owner) { create(:user) }
+
+      context 'when user is master of the project' do
+        before do
+          project.team << [user, :master]
+        end
+
+        it_behaves_like 'allows to manage trigger'
+      end
+
+      context 'when user is developer of the project' do
+        before do
+          project.team << [user, :developer]
+        end
+
+        it_behaves_like 'disallows to admin and manage trigger'
+      end
+
+      context 'when user is not member of the project' do
+        it_behaves_like 'disallows to admin and manage trigger'
+      end
+    end
+  end
+end
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d5761390d39fa122c1572aff07d959309f260801
--- /dev/null
+++ b/spec/policies/user_policy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe UserPolicy, models: true do
+  let(:current_user) { create(:user) }
+  let(:user) { create(:user) }
+
+  subject { described_class.abilities(current_user, user).to_set }
+
+  describe "reading a user's information" do
+    it { is_expected.to include(:read_user) }
+  end
+
+  describe "destroying a user" do
+    context "when a regular user tries to destroy another regular user" do
+      it { is_expected.not_to include(:destroy_user) }
+    end
+
+    context "when a regular user tries to destroy themselves" do
+      let(:current_user) { user }
+
+      it { is_expected.to include(:destroy_user) }
+    end
+
+    context "when an admin user tries to destroy a regular user" do
+      let(:current_user) { create(:user, :admin) }
+
+      it { is_expected.to include(:destroy_user) }
+    end
+
+    context "when an admin user tries to destroy a ghost user" do
+      let(:current_user) { create(:user, :admin) }
+      let(:user) { create(:user, :ghost) }
+
+      it { is_expected.not_to include(:destroy_user) }
+    end
+  end
+end
diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6443f86b6a1e6585caf1db65f4b4c23861188e45
--- /dev/null
+++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::Settings::DeployKeysPresenter do
+  let(:project) { create(:empty_project) }
+  let(:user) { create(:user) }
+  let(:deploy_key)  { create(:deploy_key, public: true) }
+
+  let!(:deploy_keys_project) do
+    create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+  end
+
+  subject(:presenter) do
+    described_class.new(project, current_user: user)
+  end
+
+  it 'inherits from Gitlab::View::Presenter::Simple' do
+    expect(described_class.superclass).to eq(Gitlab::View::Presenter::Simple)
+  end
+
+  describe '#enabled_keys' do
+    it 'returns currently enabled keys' do
+      expect(presenter.enabled_keys).to eq [deploy_keys_project.deploy_key]
+    end
+
+    it 'does not contain enabled_keys inside available_keys' do
+      expect(presenter.available_keys).not_to include deploy_key
+    end
+
+    it 'returns the enabled_keys size' do
+      expect(presenter.enabled_keys_size).to eq(1)
+    end
+
+    it 'returns true if there is any enabled_keys' do
+      expect(presenter.any_keys_enabled?).to eq(true)
+    end
+  end
+
+  describe '#available_keys/#available_project_keys' do
+    let(:other_deploy_key) { create(:another_deploy_key) }
+
+    before do
+      project_key = create(:deploy_keys_project, deploy_key: other_deploy_key)
+      project_key.project.add_developer(user)
+    end
+
+    it 'returns the current available_keys' do
+      expect(presenter.available_keys).not_to be_empty
+    end
+
+    it 'returns the current available_project_keys' do
+      expect(presenter.available_project_keys).not_to be_empty
+    end
+
+    it 'returns false if any available_project_keys are enabled' do
+      expect(presenter.any_available_project_keys_enabled?).to eq(true)
+    end
+
+    it 'returns the available_project_keys size' do
+      expect(presenter.available_project_keys_size).to eq(1)
+    end
+
+    it 'shows if there is an available key' do
+      expect(presenter.key_available?(deploy_key)).to eq(false)
+    end
+  end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 919c98d6437a05239948718637746eb8e74140a8..46edbd49b289483d84d2171a3f1e9c478c0b7d88 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -200,7 +200,7 @@ describe API::AccessRequests, api: true  do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
 
-            expect(response).to have_http_status(200)
+            expect(response).to have_http_status(204)
           end.to change { source.requesters.count }.by(-1)
         end
       end
@@ -210,7 +210,7 @@ describe API::AccessRequests, api: true  do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
 
-            expect(response).to have_http_status(200)
+            expect(response).to have_http_status(204)
           end.to change { source.requesters.count }.by(-1)
         end
 
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
index be4bc39ada20cbb7a9617f0baef1093ffc1a4c4c..f5265ea60ff4aee2d4f167df49d093100360d665 100644
--- a/spec/requests/api/api_internal_helpers_spec.rb
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -21,7 +21,7 @@ describe ::API::Helpers::InternalHelpers do
         # Relative and absolute storage paths, with and without trailing /
         ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
           context "storage path is #{storage_path}" do
-            subject { clean_project_path(project_path, [storage_path]) }
+            subject { clean_project_path(project_path, [{ 'path' => storage_path }]) }
 
             it { is_expected.to eq(expected) }
           end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 6cc1ef315db895646741cb4dccb5daa1b201a5f0..f4d4a8a2cc7e7544fba1eccff04d7d9bb2f1f2f5 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -15,7 +15,7 @@ describe API::AwardEmoji, api: true  do
   describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
     context 'on an issue' do
       it "returns an array of award_emoji" do
-        get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+        get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
 
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -31,7 +31,7 @@ describe API::AwardEmoji, api: true  do
 
     context 'on a merge request' do
       it "returns an array of award_emoji" do
-        get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+        get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user)
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
@@ -57,7 +57,7 @@ describe API::AwardEmoji, api: true  do
       it 'returns a status code 404' do
         user1 = create(:user)
 
-        get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+        get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1)
 
         expect(response).to have_http_status(404)
       end
@@ -68,7 +68,7 @@ describe API::AwardEmoji, api: true  do
     let!(:rocket)  { create(:award_emoji, awardable: note, name: 'rocket') }
 
     it 'returns an array of award emoji' do
-      get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+      get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user)
 
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -79,7 +79,7 @@ describe API::AwardEmoji, api: true  do
   describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
     context 'on an issue' do
       it "returns the award emoji" do
-        get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+        get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(award_emoji.name)
@@ -88,7 +88,7 @@ describe API::AwardEmoji, api: true  do
       end
 
       it "returns a 404 error if the award is not found" do
-        get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+        get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
 
         expect(response).to have_http_status(404)
       end
@@ -96,7 +96,7 @@ describe API::AwardEmoji, api: true  do
 
     context 'on a merge request' do
       it 'returns the award emoji' do
-        get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+        get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(downvote.name)
@@ -123,7 +123,7 @@ describe API::AwardEmoji, api: true  do
       it 'returns a status code 404' do
         user1 = create(:user)
 
-        get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+        get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1)
 
         expect(response).to have_http_status(404)
       end
@@ -134,7 +134,7 @@ describe API::AwardEmoji, api: true  do
     let!(:rocket)  { create(:award_emoji, awardable: note, name: 'rocket') }
 
     it 'returns an award emoji' do
-      get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+      get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
 
       expect(response).to have_http_status(200)
       expect(json_response).not_to be_an Array
@@ -147,7 +147,7 @@ describe API::AwardEmoji, api: true  do
 
     context "on an issue" do
       it "creates a new award emoji" do
-        post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish'
 
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq('blowfish')
@@ -155,13 +155,13 @@ describe API::AwardEmoji, api: true  do
       end
 
       it "returns a 400 bad request error if the name is not given" do
-        post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+        post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
 
         expect(response).to have_http_status(400)
       end
 
       it "returns a 401 unauthorized error if the user is not authenticated" do
-        post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup'
 
         expect(response).to have_http_status(401)
       end
@@ -173,15 +173,15 @@ describe API::AwardEmoji, api: true  do
       end
 
       it "normalizes +1 as thumbsup award" do
-        post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: '+1'
 
         expect(issue.award_emoji.last.name).to eq("thumbsup")
       end
 
       context 'when the emoji already has been awarded' do
         it 'returns a 404 status code' do
-          post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
-          post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+          post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
+          post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
 
           expect(response).to have_http_status(404)
           expect(json_response["message"]).to match("has already been taken")
@@ -207,7 +207,7 @@ describe API::AwardEmoji, api: true  do
 
     it 'creates a new award emoji' do
       expect do
-        post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
       end.to change { note.award_emoji.count }.from(0).to(1)
 
       expect(response).to have_http_status(201)
@@ -215,21 +215,21 @@ describe API::AwardEmoji, api: true  do
     end
 
     it "it returns 404 error when user authored note" do
-      post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+      post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
 
       expect(response).to have_http_status(404)
     end
 
     it "normalizes +1 as thumbsup award" do
-      post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+      post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: '+1'
 
       expect(note.award_emoji.last.name).to eq("thumbsup")
     end
 
     context 'when the emoji already has been awarded' do
       it 'returns a 404 status code' do
-        post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
-        post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+        post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
 
         expect(response).to have_http_status(404)
         expect(json_response["message"]).to match("has already been taken")
@@ -241,14 +241,14 @@ describe API::AwardEmoji, api: true  do
     context 'when the awardable is an Issue' do
       it 'deletes the award' do
         expect do
-          delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
-        end.to change { issue.award_emoji.count }.from(1).to(0)
+          delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
 
-        expect(response).to have_http_status(200)
+          expect(response).to have_http_status(204)
+        end.to change { issue.award_emoji.count }.from(1).to(0)
       end
 
       it 'returns a 404 error when the award emoji can not be found' do
-        delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+        delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
 
         expect(response).to have_http_status(404)
       end
@@ -257,14 +257,14 @@ describe API::AwardEmoji, api: true  do
     context 'when the awardable is a Merge Request' do
       it 'deletes the award' do
         expect do
-          delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
-        end.to change { merge_request.award_emoji.count }.from(1).to(0)
+          delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
 
-        expect(response).to have_http_status(200)
+          expect(response).to have_http_status(204)
+        end.to change { merge_request.award_emoji.count }.from(1).to(0)
       end
 
       it 'returns a 404 error when note id not found' do
-        delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+        delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user)
 
         expect(response).to have_http_status(404)
       end
@@ -277,9 +277,9 @@ describe API::AwardEmoji, api: true  do
       it 'deletes the award' do
         expect do
           delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
-        end.to change { snippet.award_emoji.count }.from(1).to(0)
 
-        expect(response).to have_http_status(200)
+          expect(response).to have_http_status(204)
+        end.to change { snippet.award_emoji.count }.from(1).to(0)
       end
     end
   end
@@ -289,10 +289,10 @@ describe API::AwardEmoji, api: true  do
 
     it 'deletes the award' do
       expect do
-        delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
-      end.to change { note.award_emoji.count }.from(1).to(0)
+        delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
 
-      expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
+      end.to change { note.award_emoji.count }.from(1).to(0)
     end
   end
 end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 71df534ebe15e7e9d90b6412209694d05ea7ce47..87c36639cd41428dbd78c33f5fdb9196ecb24a7a 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -195,8 +195,7 @@ describe API::Boards, api: true  do
       it "deletes the list if an admin requests it" do
         delete api("#{base_url}/#{dev_list.id}", owner)
 
-        expect(response).to have_http_status(200)
-        expect(json_response['position']).to eq(1)
+        expect(response).to have_http_status(204)
       end
     end
   end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5571f6cc10747e6a3fe60453794fac3a454fcc28..a70f7beaae0d387e0ba37fe2344b8a9ff2779fb3 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,77 +5,146 @@ describe API::Branches, api: true  do
   include ApiHelpers
 
   let(:user) { create(:user) }
-  let(:user2) { create(:user) }
   let!(:project) { create(:project, :repository, creator: user) }
   let!(:master) { create(:project_member, :master, user: user, project: project) }
-  let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+  let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
   let!(:branch_name) { 'feature' }
   let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
-  let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+  let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] }
 
   describe "GET /projects/:id/repository/branches" do
-    it "returns an array of project branches" do
-      project.repository.expire_all_method_caches
+    let(:route) { "/projects/#{project.id}/repository/branches" }
 
-      get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+    shared_examples_for 'repository branches' do
+      it 'returns the repository branches' do
+        get api(route, current_user), per_page: 100
 
-      expect(response).to have_http_status(200)
-      expect(response).to include_pagination_headers
-      expect(json_response).to be_an Array
-      branch_names = json_response.map { |x| x['name'] }
-      expect(branch_names).to match_array(project.repository.branch_names)
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        branch_names = json_response.map { |x| x['name'] }
+        expect(branch_names).to match_array(project.repository.branch_names)
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get api(route, current_user) }
+        end
+      end
     end
-  end
 
-  describe "GET /projects/:id/repository/branches/:branch" do
-    it "returns the branch information for a single branch" do
-      get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
-      expect(response).to have_http_status(200)
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository branches' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
 
-      expect(json_response['name']).to eq(branch_name)
-      json_commit = json_response['commit']
-      expect(json_commit['id']).to eq(branch_sha)
-      expect(json_commit).to have_key('short_id')
-      expect(json_commit).to have_key('title')
-      expect(json_commit).to have_key('message')
-      expect(json_commit).to have_key('author_name')
-      expect(json_commit).to have_key('author_email')
-      expect(json_commit).to have_key('authored_date')
-      expect(json_commit).to have_key('committer_name')
-      expect(json_commit).to have_key('committer_email')
-      expect(json_commit).to have_key('committed_date')
-      expect(json_commit).to have_key('parent_ids')
-      expect(json_response['merged']).to eq(false)
-      expect(json_response['protected']).to eq(false)
-      expect(json_response['developers_can_push']).to eq(false)
-      expect(json_response['developers_can_merge']).to eq(false)
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
     end
 
-    it "returns the branch information for a single branch with dots in the name" do
-      get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository branches' do
+        let(:current_user) { user }
+      end
+    end
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq("with.1.2.3")
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route, guest) }
+      end
     end
+  end
+
+  describe "GET /projects/:id/repository/branches/:branch" do
+    let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" }
 
-    context 'on a merged branch' do
-      it "returns the branch information for a single branch" do
-        get api("/projects/#{project.id}/repository/branches/merge-test", user)
+    shared_examples_for 'repository branch' do |merged: false|
+      it 'returns the repository branch' do
+        get api(route, current_user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq('merge-test')
-        expect(json_response['merged']).to eq(true)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['merged']).to eq(merged)
+        expect(json_response['protected']).to eq(false)
+        expect(json_response['developers_can_push']).to eq(false)
+        expect(json_response['developers_can_merge']).to eq(false)
+
+        json_commit = json_response['commit']
+        expect(json_commit['id']).to eq(branch_sha)
+        expect(json_commit).to have_key('short_id')
+        expect(json_commit).to have_key('title')
+        expect(json_commit).to have_key('message')
+        expect(json_commit).to have_key('author_name')
+        expect(json_commit).to have_key('author_email')
+        expect(json_commit).to have_key('authored_date')
+        expect(json_commit).to have_key('committer_name')
+        expect(json_commit).to have_key('committer_email')
+        expect(json_commit).to have_key('committed_date')
+        expect(json_commit).to have_key('parent_ids')
+      end
+
+      context 'when branch does not exist' do
+        let(:branch_name) { 'unknown' }
+
+        it_behaves_like '404 response' do
+          let(:request) { get api(route, current_user) }
+          let(:message) { '404 Branch Not Found' }
+        end
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get api(route, current_user) }
+        end
       end
     end
 
-    it "returns a 403 error if guest" do
-      get api("/projects/#{project.id}/repository/branches", user2)
-      expect(response).to have_http_status(403)
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository branch' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
     end
 
-    it "returns a 404 error if branch is not available" do
-      get api("/projects/#{project.id}/repository/branches/unknown", user)
-      expect(response).to have_http_status(404)
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+
+    context 'when authenticated', 'as a developer' do
+      let(:current_user) { user }
+      it_behaves_like 'repository branch'
+
+      context 'when branch contains a dot' do
+        let(:branch_name) { branch_with_dot.name }
+        let(:branch_sha) { project.commit('master').sha }
+
+        it_behaves_like 'repository branch'
+      end
+
+      context 'when branch is merged' do
+        let(:branch_name) { 'merge-test' }
+        let(:branch_sha) { project.commit('merge-test').sha }
+
+        it_behaves_like 'repository branch', merged: true
+      end
+    end
+
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route, guest) }
+      end
     end
   end
 
@@ -93,10 +162,10 @@ describe API::Branches, api: true  do
       end
 
       it "protects a single branch with dots in the name" do
-        put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+        put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq("with.1.2.3")
+        expect(json_response['name']).to eq(branch_with_dot.name)
         expect(json_response['protected']).to eq(true)
       end
 
@@ -234,7 +303,7 @@ describe API::Branches, api: true  do
     end
 
     it "returns a 403 error if guest" do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
+      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest)
       expect(response).to have_http_status(403)
     end
   end
@@ -250,10 +319,10 @@ describe API::Branches, api: true  do
     end
 
     it "update branches with dots in branch name" do
-      put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+      put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user)
 
       expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq("with.1.2.3")
+      expect(json_response['name']).to eq(branch_with_dot.name)
       expect(json_response['protected']).to eq(false)
     end
 
@@ -282,7 +351,7 @@ describe API::Branches, api: true  do
     end
 
     it "denies for user without push access" do
-      post api("/projects/#{project.id}/repository/branches", user2),
+      post api("/projects/#{project.id}/repository/branches", guest),
            branch: branch_name,
            ref: branch_sha
       expect(response).to have_http_status(403)
@@ -325,15 +394,14 @@ describe API::Branches, api: true  do
 
     it "removes branch" do
       delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
-      expect(response).to have_http_status(200)
-      expect(json_response['branch']).to eq(branch_name)
+
+      expect(response).to have_http_status(204)
     end
 
     it "removes a branch with dots in the branch name" do
-      delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+      delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
 
-      expect(response).to have_http_status(200)
-      expect(json_response['branch']).to eq("with.1.2.3")
+      expect(response).to have_http_status(204)
     end
 
     it 'returns 404 if branch not exists' do
@@ -360,13 +428,15 @@ describe API::Branches, api: true  do
       allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
     end
 
-    it 'returns 200' do
+    it 'returns 202 with json body' do
       delete api("/projects/#{project.id}/repository/merged_branches", user)
-      expect(response).to have_http_status(200)
+
+      expect(response).to have_http_status(202)
+      expect(json_response['message']).to eql('202 Accepted')
     end
 
     it 'returns a 403 error if guest' do
-      delete api("/projects/#{project.id}/repository/merged_branches", user2)
+      delete api("/projects/#{project.id}/repository/merged_branches", guest)
       expect(response).to have_http_status(403)
     end
   end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 921d8714173a2f6636959c70d3f49d9190eb3e02..024fa66848c259e5ad712308213c64fca378f517 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -174,8 +174,11 @@ describe API::BroadcastMessages, api: true do
     end
 
     it 'deletes the broadcast message for admins' do
-      expect { delete api("/broadcast_messages/#{message.id}", admin) }
-        .to change { BroadcastMessage.count }.by(-1)
+      expect do
+        delete api("/broadcast_messages/#{message.id}", admin)
+
+        expect(response).to have_http_status(204)
+      end.to change { BroadcastMessage.count }.by(-1)
     end
   end
 end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 81a8856b8f1154c9f7b5b22396a7c37ec7cb8824..d8b3cc041a533806b1ea6df0775293f72799e7d8 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -151,26 +151,62 @@ describe API::CommitStatuses, api: true do
       end
 
       context 'with all optional parameters' do
-        before do
-          optional_params = { state: 'success',
-                              context: 'coverage',
-                              ref: 'develop',
-                              description: 'test',
-                              coverage: 80.0,
-                              target_url: 'http://gitlab.com/status' }
-
-          post api(post_url, developer), optional_params
+        context 'when creating a commit status' do
+          it 'creates commit status' do
+            post api(post_url, developer), {
+              state: 'success',
+              context: 'coverage',
+              ref: 'develop',
+              description: 'test',
+              coverage: 80.0,
+              target_url: 'http://gitlab.com/status'
+            }
+
+            expect(response).to have_http_status(201)
+            expect(json_response['sha']).to eq(commit.id)
+            expect(json_response['status']).to eq('success')
+            expect(json_response['name']).to eq('coverage')
+            expect(json_response['ref']).to eq('develop')
+            expect(json_response['coverage']).to eq(80.0)
+            expect(json_response['description']).to eq('test')
+            expect(json_response['target_url']).to eq('http://gitlab.com/status')
+          end
         end
 
-        it 'creates commit status' do
-          expect(response).to have_http_status(201)
-          expect(json_response['sha']).to eq(commit.id)
-          expect(json_response['status']).to eq('success')
-          expect(json_response['name']).to eq('coverage')
-          expect(json_response['ref']).to eq('develop')
-          expect(json_response['coverage']).to eq(80.0)
-          expect(json_response['description']).to eq('test')
-          expect(json_response['target_url']).to eq('http://gitlab.com/status')
+        context 'when updatig a commit status' do
+          before do
+            post api(post_url, developer), {
+              state: 'running',
+              context: 'coverage',
+              ref: 'develop',
+              description: 'coverage test',
+              coverage: 0.0,
+              target_url: 'http://gitlab.com/status'
+            }
+
+            post api(post_url, developer), {
+              state: 'success',
+              name: 'coverage',
+              ref: 'develop',
+              description: 'new description',
+              coverage: 90.0
+            }
+          end
+
+          it 'updates a commit status' do
+            expect(response).to have_http_status(201)
+            expect(json_response['sha']).to eq(commit.id)
+            expect(json_response['status']).to eq('success')
+            expect(json_response['name']).to eq('coverage')
+            expect(json_response['ref']).to eq('develop')
+            expect(json_response['coverage']).to eq(90.0)
+            expect(json_response['description']).to eq('new description')
+            expect(json_response['target_url']).to eq('http://gitlab.com/status')
+          end
+
+          it 'does not create a new commit status' do
+            expect(CommitStatus.count).to eq 1
+          end
         end
       end
 
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 8b3dfedc5a9f5a563e8faf4ec34d0362a863bde6..a10d876ffad105a42568211ef6aceb9d6e248b67 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -19,6 +19,7 @@ describe API::Commits, api: true  do
 
       it "returns project commits" do
         commit = project.repository.commit
+
         get api("/projects/#{project.id}/repository/commits", user)
 
         expect(response).to have_http_status(200)
@@ -27,6 +28,16 @@ describe API::Commits, api: true  do
         expect(json_response.first['committer_name']).to eq(commit.committer_name)
         expect(json_response.first['committer_email']).to eq(commit.committer_email)
       end
+
+      it 'include correct pagination headers' do
+        commit_count = project.repository.count_commits(ref: 'master').to_s
+
+        get api("/projects/#{project.id}/repository/commits", user)
+
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+        expect(response.headers['X-Page']).to eql('1')
+      end
     end
 
     context "unauthorized user" do
@@ -39,14 +50,26 @@ describe API::Commits, api: true  do
     context "since optional parameter" do
       it "returns project commits since provided parameter" do
         commits = project.repository.commits("master")
-        since = commits.second.created_at
+        after = commits.second.created_at
 
-        get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
+        get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
 
         expect(json_response.size).to eq 2
         expect(json_response.first["id"]).to eq(commits.first.id)
         expect(json_response.second["id"]).to eq(commits.second.id)
       end
+
+      it 'include correct pagination headers' do
+        commits = project.repository.commits("master")
+        after = commits.second.created_at
+        commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
+
+        get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
+
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+        expect(response.headers['X-Page']).to eql('1')
+      end
     end
 
     context "until optional parameter" do
@@ -65,6 +88,18 @@ describe API::Commits, api: true  do
         expect(json_response.first["id"]).to eq(commits.second.id)
         expect(json_response.second["id"]).to eq(commits.third.id)
       end
+
+      it 'include correct pagination headers' do
+        commits = project.repository.commits("master")
+        before = commits.second.created_at
+        commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
+
+        get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+        expect(response.headers['X-Page']).to eql('1')
+      end
     end
 
     context "invalid xmlschema date parameters" do
@@ -79,16 +114,71 @@ describe API::Commits, api: true  do
     context "path optional parameter" do
       it "returns project commits matching provided path parameter" do
         path = 'files/ruby/popen.rb'
+        commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
 
         get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
 
         expect(json_response.size).to eq(3)
         expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+      end
+
+      it 'include correct pagination headers' do
+        path = 'files/ruby/popen.rb'
+        commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
+
+        get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+        expect(response.headers['X-Page']).to eql('1')
+      end
+    end
+
+    context 'with pagination params' do
+      let(:page) { 1 }
+      let(:per_page) { 5 }
+      let(:ref_name) { 'master' }
+      let!(:request) do
+        get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+      end
+
+      it 'returns correct headers' do
+        commit_count = project.repository.count_commits(ref: ref_name).to_s
+
+        expect(response).to include_pagination_headers
+        expect(response.headers['X-Total']).to eq(commit_count)
+        expect(response.headers['X-Page']).to eq('1')
+        expect(response.headers['Link']).to match(/page=1&per_page=5/)
+        expect(response.headers['Link']).to match(/page=2&per_page=5/)
+      end
+
+      context 'viewing the first page' do
+        it 'returns the first 5 commits' do
+          commit = project.repository.commit
+
+          expect(json_response.size).to eq(per_page)
+          expect(json_response.first['id']).to eq(commit.id)
+          expect(response.headers['X-Page']).to eq('1')
+        end
+      end
+
+      context 'viewing the third page' do
+        let(:page) { 3 }
+
+        it 'returns the third 5 commits' do
+          commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
+
+          expect(json_response.size).to eq(per_page)
+          expect(json_response.first['id']).to eq(commit.id)
+          expect(response.headers['X-Page']).to eq('3')
+        end
       end
     end
   end
 
-  describe "Create a commit with multiple files and actions" do
+  describe "POST /projects/:id/repository/commits" do
     let!(:url) { "/projects/#{project.id}/repository/commits" }
 
     it 'returns a 403 unauthorized for user without permissions' do
@@ -103,7 +193,7 @@ describe API::Commits, api: true  do
       expect(response).to have_http_status(400)
     end
 
-    context :create do
+    describe 'create' do
       let(:message) { 'Created file' }
       let!(:invalid_c_params) do
         {
@@ -147,8 +237,8 @@ describe API::Commits, api: true  do
         expect(response).to have_http_status(400)
       end
 
-      context 'with project path in URL' do
-        let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+      context 'with project path containing a dot in URL' do
+        let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
 
         it 'a new file in project repo' do
           post api(url, user), valid_c_params
@@ -158,7 +248,7 @@ describe API::Commits, api: true  do
       end
     end
 
-    context :delete do
+    describe 'delete' do
       let(:message) { 'Deleted file' }
       let!(:invalid_d_params) do
         {
@@ -199,7 +289,7 @@ describe API::Commits, api: true  do
       end
     end
 
-    context :move do
+    describe 'move' do
       let(:message) { 'Moved file' }
       let!(:invalid_m_params) do
         {
@@ -244,7 +334,7 @@ describe API::Commits, api: true  do
       end
     end
 
-    context :update do
+    describe 'update' do
       let(:message) { 'Updated file' }
       let!(:invalid_u_params) do
         {
@@ -287,7 +377,7 @@ describe API::Commits, api: true  do
       end
     end
 
-    context "multiple operations" do
+    describe 'multiple operations' do
       let(:message) { 'Multiple actions' }
       let!(:invalid_mo_params) do
         {
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 7e682e91bd1a70e45d53807ac4727e37d86f305e..4f4b18cf0e0c122af4f6d8dcb59c5e9571814f2b 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -116,6 +116,8 @@ describe API::DeployKeys, api: true  do
     it 'should delete existing key' do
       expect do
         delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+        expect(response).to have_http_status(204)
       end.to change{ project.deploy_keys.count }.by(-1)
     end
 
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index bd9ecaf26856c99ed80de5920f426786efbddd80..f6fd567eca573bd49ec5a4d1900f564860de31c5 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -1,17 +1,23 @@
 require 'spec_helper'
 
-describe API::API, api: true  do
+describe API::API, api: true do
   include ApiHelpers
 
   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, scopes: "api" }
 
-  describe "when unauthenticated" do
+  describe "unauthenticated" do
     it "returns authentication success" do
       get api("/user"), access_token: token.token
       expect(response).to have_http_status(200)
     end
+
+    include_examples 'user login request with unique ip limit' do
+      def request
+        get api('/user'), access_token: token.token
+      end
+    end
   end
 
   describe "when token invalid" do
@@ -26,5 +32,29 @@ describe API::API, api: true  do
       get api("/user", user)
       expect(response).to have_http_status(200)
     end
+
+    include_examples 'user login request with unique ip limit' do
+      def request
+        get api('/user', user)
+      end
+    end
+  end
+
+  describe "when user is blocked" do
+    it "returns authentication error" do
+      user.block
+      get api("/user"), access_token: token.token
+
+      expect(response).to have_http_status(401)
+    end
+  end
+
+  describe "when user is ldap_blocked" do
+    it "returns authentication error" do
+      user.ldap_block
+      get api("/user"), access_token: token.token
+
+      expect(response).to have_http_status(401)
+    end
   end
 end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index d0958d39d44efa3b5e772c1cf9107c131d9524e0..b54ee8e8b85002b017fb04d61e911f44e09842c7 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -15,6 +15,8 @@ describe API::Environments, api: true  do
   describe 'GET /projects/:id/environments' do
     context 'as member of the project' do
       it 'returns project environments' do
+        project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+
         get api("/projects/#{project.id}/environments", user)
 
         expect(response).to have_http_status(200)
@@ -23,7 +25,7 @@ describe API::Environments, api: true  do
         expect(json_response.size).to eq(1)
         expect(json_response.first['name']).to eq(environment.name)
         expect(json_response.first['external_url']).to eq(environment.external_url)
-        expect(json_response.first['project']['id']).to eq(project.id)
+        expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
       end
     end
 
@@ -122,7 +124,7 @@ describe API::Environments, api: true  do
       it 'returns a 200 for an existing environment' do
         delete api("/projects/#{project.id}/environments/#{environment.id}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
       end
 
       it 'returns a 404 for non existing id' do
@@ -141,4 +143,39 @@ describe API::Environments, api: true  do
       end
     end
   end
+
+  describe 'POST /projects/:id/environments/:environment_id/stop' do
+    context 'as a master' do
+      context 'with a stoppable environment' do
+        before do
+          environment.update(state: :available)
+
+          post api("/projects/#{project.id}/environments/#{environment.id}/stop", user)
+        end
+
+        it 'returns a 200' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'actually stops the environment' do
+          expect(environment.reload).to be_stopped
+        end
+      end
+
+      it 'returns a 404 for non existing id' do
+        post api("/projects/#{project.id}/environments/12345/stop", user)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 Not found')
+      end
+    end
+
+    context 'a non member' do
+      it 'rejects the request' do
+        post api("/projects/#{project.id}/environments/#{environment.id}/stop", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index a8ce04304018466d12a70a2cf815e626476fe571..a7fad7f0bdb9eb2a2a010d6181c322778843e5ea 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,10 +5,9 @@ describe API::Files, api: true  do
   let(:user) { create(:user) }
   let!(:project) { create(:project, :repository, namespace: user.namespace ) }
   let(:guest) { create(:user) { |u| project.add_guest(u) } }
-  let(:file_path) { 'files/ruby/popen.rb' }
+  let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
   let(:params) do
     {
-      file_path: file_path,
       ref: 'master'
     }
   end
@@ -30,36 +29,54 @@ describe API::Files, api: true  do
 
   before { project.team << [user, :developer] }
 
-  describe "GET /projects/:id/repository/files" do
-    let(:route) { "/projects/#{project.id}/repository/files" }
+  def route(file_path = nil)
+    "/projects/#{project.id}/repository/files/#{file_path}"
+  end
 
+  describe "GET /projects/:id/repository/files/:file_path" do
     shared_examples_for 'repository files' do
-      it "returns file info" do
-        get api(route, current_user), params
+      it 'returns file attributes as json' do
+        get api(route(file_path), current_user), params
 
         expect(response).to have_http_status(200)
-        expect(json_response['file_path']).to eq(file_path)
+        expect(json_response['file_path']).to eq(CGI.unescape(file_path))
         expect(json_response['file_name']).to eq('popen.rb')
         expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
         expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
       end
 
-      context 'when no params are given' do
+      it 'returns file by commit sha' do
+        # This file is deleted on HEAD
+        file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+        params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+
+        get api(route(file_path), current_user), params
+
+        expect(response).to have_http_status(200)
+        expect(json_response['file_name']).to eq('commit.js.coffee')
+        expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+      end
+
+      it 'returns raw file info' do
+        url = route(file_path) + "/raw"
+        expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+        get api(url, current_user), params
+
+        expect(response).to have_http_status(200)
+      end
+
+      context 'when mandatory params are not given' do
         it_behaves_like '400 response' do
-          let(:request) { get api(route, current_user) }
+          let(:request) { get api(route("any%2Ffile"), current_user) }
         end
       end
 
       context 'when file_path does not exist' do
-        let(:params) do
-          {
-            file_path: 'app/models/application.rb',
-            ref: 'master',
-          }
-        end
+        let(:params) { { ref: 'master' } }
 
         it_behaves_like '404 response' do
-          let(:request) { get api(route, current_user), params }
+          let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
           let(:message) { '404 File Not Found' }
         end
       end
@@ -68,7 +85,7 @@ describe API::Files, api: true  do
         include_context 'disabled repository'
 
         it_behaves_like '403 response' do
-          let(:request) { get api(route, current_user), params }
+          let(:request) { get api(route(file_path), current_user), params }
         end
       end
     end
@@ -82,7 +99,7 @@ describe API::Files, api: true  do
 
     context 'when unauthenticated', 'and project is private' do
       it_behaves_like '404 response' do
-        let(:request) { get api(route), params }
+        let(:request) { get api(route(file_path)), params }
         let(:message) { '404 Project Not Found' }
       end
     end
@@ -95,42 +112,115 @@ describe API::Files, api: true  do
 
     context 'when authenticated', 'as a guest' do
       it_behaves_like '403 response' do
-        let(:request) { get api(route, guest), params }
+        let(:request) { get api(route(file_path), guest), params }
+      end
+    end
+  end
+
+  describe "GET /projects/:id/repository/files/:file_path/raw" do
+    shared_examples_for 'repository raw files' do
+      it 'returns raw file info' do
+        url = route(file_path) + "/raw"
+        expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+        get api(url, current_user), params
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns file by commit sha' do
+        # This file is deleted on HEAD
+        file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+        params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+        expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+        get api(route(file_path) + "/raw", current_user), params
+
+        expect(response).to have_http_status(200)
+      end
+
+      context 'when mandatory params are not given' do
+        it_behaves_like '400 response' do
+          let(:request) { get api(route("any%2Ffile"), current_user) }
+        end
+      end
+
+      context 'when file_path does not exist' do
+        let(:params) { { ref: 'master' } }
+
+        it_behaves_like '404 response' do
+          let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
+          let(:message) { '404 File Not Found' }
+        end
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get api(route(file_path), current_user), params }
+        end
+      end
+    end
+
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository raw files' do
+        let(:project) { create(:project, :public) }
+        let(:current_user) { nil }
+      end
+    end
+
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route(file_path)), params }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository raw files' do
+        let(:current_user) { user }
+      end
+    end
+
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route(file_path), guest), params }
       end
     end
   end
 
-  describe "POST /projects/:id/repository/files" do
+  describe "POST /projects/:id/repository/files/:file_path" do
+    let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
     let(:valid_params) do
       {
-        file_path: 'newfile.rb',
-        branch: 'master',
-        content: 'puts 8',
-        commit_message: 'Added newfile'
+        branch: "master",
+        content: "puts 8",
+        commit_message: "Added newfile"
       }
     end
 
     it "creates a new file in project repo" do
-      post api("/projects/#{project.id}/repository/files", user), valid_params
+      post api(route(file_path), user), valid_params
 
       expect(response).to have_http_status(201)
-      expect(json_response['file_path']).to eq('newfile.rb')
+      expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
       last_commit = project.repository.commit.raw
       expect(last_commit.author_email).to eq(user.email)
       expect(last_commit.author_name).to eq(user.name)
     end
 
-    it "returns a 400 bad request if no params given" do
-      post api("/projects/#{project.id}/repository/files", user)
+    it "returns a 400 bad request if no mandatory params given" do
+      post api(route("any%2Etxt"), user)
 
       expect(response).to have_http_status(400)
     end
 
     it "returns a 400 if editor fails to create file" do
-      allow_any_instance_of(Repository).to receive(:commit_file).
+      allow_any_instance_of(Repository).to receive(:create_file).
         and_return(false)
 
-      post api("/projects/#{project.id}/repository/files", user), valid_params
+      post api(route("any%2Etxt"), user), valid_params
 
       expect(response).to have_http_status(400)
     end
@@ -139,7 +229,7 @@ describe API::Files, api: true  do
       it "creates a new file with the specified author" do
         valid_params.merge!(author_email: author_email, author_name: author_name)
 
-        post api("/projects/#{project.id}/repository/files", user), valid_params
+        post api(route("new_file_with_author%2Etxt"), user), valid_params
 
         expect(response).to have_http_status(201)
         last_commit = project.repository.commit.raw
@@ -147,12 +237,25 @@ describe API::Files, api: true  do
         expect(last_commit.author_name).to eq(author_name)
       end
     end
+
+    context 'when the repo is empty' do
+      let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+
+      it "creates a new file in project repo" do
+        post api(route("newfile%2Erb"), user), valid_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['file_path']).to eq('newfile.rb')
+        last_commit = project.repository.commit.raw
+        expect(last_commit.author_email).to eq(user.email)
+        expect(last_commit.author_name).to eq(user.name)
+      end
+    end
   end
 
   describe "PUT /projects/:id/repository/files" do
     let(:valid_params) do
       {
-        file_path: file_path,
         branch: 'master',
         content: 'puts 8',
         commit_message: 'Changed file'
@@ -160,17 +263,17 @@ describe API::Files, api: true  do
     end
 
     it "updates existing file in project repo" do
-      put api("/projects/#{project.id}/repository/files", user), valid_params
+      put api(route(file_path), user), valid_params
 
       expect(response).to have_http_status(200)
-      expect(json_response['file_path']).to eq(file_path)
+      expect(json_response['file_path']).to eq(CGI.unescape(file_path))
       last_commit = project.repository.commit.raw
       expect(last_commit.author_email).to eq(user.email)
       expect(last_commit.author_name).to eq(user.name)
     end
 
     it "returns a 400 bad request if no params given" do
-      put api("/projects/#{project.id}/repository/files", user)
+      put api(route(file_path), user)
 
       expect(response).to have_http_status(400)
     end
@@ -179,7 +282,7 @@ describe API::Files, api: true  do
       it "updates a file with the specified author" do
         valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
 
-        put api("/projects/#{project.id}/repository/files", user), valid_params
+        put api(route(file_path), user), valid_params
 
         expect(response).to have_http_status(200)
         last_commit = project.repository.commit.raw
@@ -192,32 +295,27 @@ describe API::Files, api: true  do
   describe "DELETE /projects/:id/repository/files" do
     let(:valid_params) do
       {
-        file_path: file_path,
         branch: 'master',
         commit_message: 'Changed file'
       }
     end
 
     it "deletes existing file in project repo" do
-      delete api("/projects/#{project.id}/repository/files", user), valid_params
+      delete api(route(file_path), user), valid_params
 
-      expect(response).to have_http_status(200)
-      expect(json_response['file_path']).to eq(file_path)
-      last_commit = project.repository.commit.raw
-      expect(last_commit.author_email).to eq(user.email)
-      expect(last_commit.author_name).to eq(user.name)
+      expect(response).to have_http_status(204)
     end
 
     it "returns a 400 bad request if no params given" do
-      delete api("/projects/#{project.id}/repository/files", user)
+      delete api(route(file_path), user)
 
       expect(response).to have_http_status(400)
     end
 
     it "returns a 400 if fails to create file" do
-      allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
+      allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
 
-      delete api("/projects/#{project.id}/repository/files", user), valid_params
+      delete api(route(file_path), user), valid_params
 
       expect(response).to have_http_status(400)
     end
@@ -226,21 +324,17 @@ describe API::Files, api: true  do
       it "removes a file with the specified author" do
         valid_params.merge!(author_email: author_email, author_name: author_name)
 
-        delete api("/projects/#{project.id}/repository/files", user), valid_params
+        delete api(route(file_path), user), valid_params
 
-        expect(response).to have_http_status(200)
-        last_commit = project.repository.commit.raw
-        expect(last_commit.author_email).to eq(author_email)
-        expect(last_commit.author_name).to eq(author_name)
+        expect(response).to have_http_status(204)
       end
     end
   end
 
   describe "POST /projects/:id/repository/files with binary file" do
-    let(:file_path) { 'test.bin' }
+    let(:file_path) { 'test%2Ebin' }
     let(:put_params) do
       {
-        file_path: file_path,
         branch: 'master',
         content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
         commit_message: 'Binary file with a \n should not be touched',
@@ -249,21 +343,20 @@ describe API::Files, api: true  do
     end
     let(:get_params) do
       {
-        file_path: file_path,
         ref: 'master',
       }
     end
 
     before do
-      post api("/projects/#{project.id}/repository/files", user), put_params
+      post api(route(file_path), user), put_params
     end
 
     it "remains unchanged" do
-      get api("/projects/#{project.id}/repository/files", user), get_params
+      get api(route(file_path), user), get_params
 
       expect(response).to have_http_status(200)
-      expect(json_response['file_path']).to eq(file_path)
-      expect(json_response['file_name']).to eq(file_path)
+      expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+      expect(json_response['file_name']).to eq(CGI.unescape(file_path))
       expect(json_response['content']).to eq(put_params[:content])
     end
   end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index a59112579e5b198c967d2537f26d183a7de61a6a..2545da7b1db9e3b7fc2a5f4ddfb25ba8068a2319 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -76,6 +76,8 @@ describe API::Groups, api: true  do
           lfs_objects_size: 234,
           build_artifacts_size: 345,
         }.stringify_keys
+        exposed_attributes = attributes.dup
+        exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
 
         project1.statistics.update!(attributes)
 
@@ -85,7 +87,7 @@ describe API::Groups, api: true  do
         expect(response).to include_pagination_headers
         expect(json_response).to be_an Array
         expect(json_response)
-          .to satisfy_one { |group| group['statistics'] == attributes }
+          .to satisfy_one { |group| group['statistics'] == exposed_attributes }
       end
     end
 
@@ -150,20 +152,10 @@ describe API::Groups, api: true  do
         expect(response_groups).to eq([group1.name, group3.name])
       end
     end
-  end
-
-  describe 'GET /groups/owned' do
-    context 'when unauthenticated' do
-      it 'returns authentication error' do
-        get api('/groups/owned')
-
-        expect(response).to have_http_status(401)
-      end
-    end
 
-    context 'when authenticated as group owner' do
+    context 'when using owned in the request' do
       it 'returns an array of groups the user owns' do
-        get api('/groups/owned', user2)
+        get api('/groups', user2), owned: true
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
@@ -186,7 +178,7 @@ describe API::Groups, api: true  do
         expect(json_response['name']).to eq(group1.name)
         expect(json_response['path']).to eq(group1.path)
         expect(json_response['description']).to eq(group1.description)
-        expect(json_response['visibility_level']).to eq(group1.visibility_level)
+        expect(json_response['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
         expect(json_response['avatar_url']).to eq(group1.avatar_url)
         expect(json_response['web_url']).to eq(group1.web_url)
         expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
@@ -303,9 +295,9 @@ describe API::Groups, api: true  do
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
         expect(json_response.length).to eq(2)
-        project_names = json_response.map { |proj| proj['name' ] }
+        project_names = json_response.map { |proj| proj['name'] }
         expect(project_names).to match_array([project1.name, project3.name])
-        expect(json_response.first['visibility_level']).to be_present
+        expect(json_response.first['visibility']).to be_present
       end
 
       it "returns the group's projects with simple representation" do
@@ -314,9 +306,9 @@ describe API::Groups, api: true  do
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
         expect(json_response.length).to eq(2)
-        project_names = json_response.map { |proj| proj['name' ] }
+        project_names = json_response.map { |proj| proj['name'] }
         expect(project_names).to match_array([project1.name, project3.name])
-        expect(json_response.first['visibility_level']).not_to be_present
+        expect(json_response.first['visibility']).not_to be_present
       end
 
       it 'filters the groups projects' do
@@ -398,7 +390,7 @@ describe API::Groups, api: true  do
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
-        project_names = json_response.map { |proj| proj['name' ] }
+        project_names = json_response.map { |proj| proj['name'] }
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
@@ -477,7 +469,7 @@ describe API::Groups, api: true  do
       it "removes group" do
         delete api("/groups/#{group1.id}", user1)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
       end
 
       it "does not remove a group if not an owner" do
@@ -506,7 +498,7 @@ describe API::Groups, api: true  do
       it "removes any existing group" do
         delete api("/groups/#{group2.id}", admin)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
       end
 
       it "does not remove a non existing group" do
@@ -519,7 +511,7 @@ describe API::Groups, api: true  do
 
   describe "POST /groups/:id/projects/:project_id" do
     let(:project) { create(:empty_project) }
-    let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+    let(:project_path) { project.full_path.gsub('/', '%2F') }
 
     before(:each) do
       allow_any_instance_of(Projects::TransferService).
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a89676fec93d4032723ddc55782e911fd5f47e43..988a57a80ea34aedf5c725309ad33a8848ae898c 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -436,7 +436,7 @@ describe API::Helpers, api: true do
 
     context 'current_user is present' do
       before do
-        expect_any_instance_of(self.class).to receive(:current_user).and_return(true)
+        expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new)
       end
 
       it 'does not raise an error' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index ffeacb15f17a81843b4a7bf1fa7e45c7bf83f28c..63ec00cdf0491969b34583161162a7cc848a9974 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -397,16 +397,53 @@ describe API::Internal, api: true  do
 
     before do
       project.team << [user, :developer]
-      get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
     end
 
     it 'returns link to create new merge request' do
+      get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
       expect(json_response).to match [{
         "branch_name" => "new_branch",
         "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
         "new_merge_request" => true
       }]
     end
+
+    it 'returns empty array if printing_merge_request_link_enabled is false' do
+      project.update!(printing_merge_request_link_enabled: false)
+
+      get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
+      expect(json_response).to eq([])
+    end
+  end
+
+  describe 'POST /notify_post_receive' do
+    let(:valid_params) do
+      { repo_path: project.repository.path, secret_token: secret_token }
+    end
+
+    before do
+      allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
+    end
+
+    it "calls the Gitaly client if it's enabled" do
+      expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+        to receive(:post_receive).with(project.repository.path)
+
+      post api("/internal/notify_post_receive"), valid_params
+
+      expect(response).to have_http_status(200)
+    end
+
+    it "returns 500 if the gitaly call fails" do
+      expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+        to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable)
+
+      post api("/internal/notify_post_receive"), valid_params
+
+      expect(response).to have_http_status(500)
+    end
   end
 
   def project_with_repo_path(path)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 56ca4c04e7d943cf3ca21824fadf2218dd4f312b..52f68fed2cce62b57a48c6cab9dad91d8efa5d51 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -153,6 +153,16 @@ describe API::Issues, api: true  do
         expect(json_response.first['state']).to eq('opened')
       end
 
+      it 'returns unlabeled issues for "No Label" label' do
+        get api("/issues", user), labels: 'No Label'
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['labels']).to be_empty
+      end
+
       it 'returns an empty array if no issue matches labels and state filters' do
         get api("/issues?labels=#{label.title}&state=closed", user)
 
@@ -212,6 +222,25 @@ describe API::Issues, api: true  do
         expect(json_response.first['id']).to eq(confidential_issue.id)
       end
 
+      it 'returns an array of issues found by iids' do
+        get api('/issues', user), iids: [closed_issue.iid]
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['id']).to eq(closed_issue.id)
+      end
+
+      it 'returns an empty array if iid does not exist' do
+        get api("/issues", user), iids: [99999]
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(0)
+      end
+
       it 'sorts by created_at descending by default' do
         get api('/issues', user)
 
@@ -251,6 +280,13 @@ describe API::Issues, api: true  do
         expect(json_response).to be_an Array
         expect(response_dates).to eq(response_dates.sort)
       end
+
+      it 'matches V4 response schema' do
+        get api('/issues', user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to match_response_schema('public_api/v4/issues')
+      end
     end
   end
 
@@ -377,6 +413,25 @@ describe API::Issues, api: true  do
       expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
     end
 
+    it 'returns an array of issues found by iids' do
+      get api(base_url, user), iids: [group_issue.iid]
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(1)
+      expect(json_response.first['id']).to eq(group_issue.id)
+    end
+
+    it 'returns an empty array if iid does not exist' do
+      get api(base_url, user), iids: [99999]
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(0)
+    end
+
     it 'returns an empty array if no group issue matches labels' do
       get api("#{base_url}?labels=foo,bar", user)
 
@@ -479,6 +534,12 @@ describe API::Issues, api: true  do
   describe "GET /projects/:id/issues" do
     let(:base_url) { "/projects/#{project.id}" }
 
+    it 'returns 404 when project does not exist' do
+      get api('/projects/1000/issues', non_member)
+
+      expect(response).to have_http_status(404)
+    end
+
     it "returns 404 on private projects for other users" do
       private_project = create(:empty_project, :private)
       create(:issue, project: private_project)
@@ -586,6 +647,25 @@ describe API::Issues, api: true  do
       expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
     end
 
+    it 'returns an array of issues found by iids' do
+      get api("#{base_url}/issues", user), iids: [issue.iid]
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(1)
+      expect(json_response.first['id']).to eq(issue.id)
+    end
+
+    it 'returns an empty array if iid does not exist' do
+      get api("#{base_url}/issues", user), iids: [99999]
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(0)
+    end
+
     it 'returns an empty array if not all labels matches' do
       get api("#{base_url}/issues?labels=#{label.title},foo", user)
 
@@ -693,9 +773,9 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe "GET /projects/:id/issues/:issue_id" do
+  describe "GET /projects/:id/issues/:issue_iid" do
     it 'exposes known attributes' do
-      get api("/projects/#{project.id}/issues/#{issue.id}", user)
+      get api("/projects/#{project.id}/issues/#{issue.iid}", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['id']).to eq(issue.id)
@@ -713,8 +793,8 @@ describe API::Issues, api: true  do
       expect(json_response['confidential']).to be_falsy
     end
 
-    it "returns a project issue by id" do
-      get api("/projects/#{project.id}/issues/#{issue.id}", user)
+    it "returns a project issue by internal id" do
+      get api("/projects/#{project.id}/issues/#{issue.iid}", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(issue.title)
@@ -726,40 +806,52 @@ describe API::Issues, api: true  do
       expect(response).to have_http_status(404)
     end
 
+    it "returns 404 if the issue ID is used" do
+      get api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     context 'confidential issues' do
       it "returns 404 for non project members" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
+
         expect(response).to have_http_status(404)
       end
 
       it "returns 404 for project members with guest role" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
+
         expect(response).to have_http_status(404)
       end
 
       it "returns confidential issue for project members" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
+
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
       it "returns confidential issue for author" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
+
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
       it "returns confidential issue for assignee" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
+
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
       it "returns confidential issue for admin" do
-        get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+        get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
@@ -775,7 +867,7 @@ describe API::Issues, api: true  do
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new issue')
       expect(json_response['description']).to be_nil
-      expect(json_response['labels']).to eq(['label', 'label2'])
+      expect(json_response['labels']).to eq(%w(label label2))
       expect(json_response['confidential']).to be_falsy
     end
 
@@ -852,29 +944,34 @@ describe API::Issues, api: true  do
       ])
     end
 
-    context 'resolving issues in a merge request' do
+    context 'resolving discussions' do
       let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
       let(:merge_request) { discussion.noteable }
       let(:project) { merge_request.source_project }
+
       before do
         project.team << [user, :master]
-        post api("/projects/#{project.id}/issues", user),
-             title: 'New Issue',
-             merge_request_for_resolving_discussions: merge_request.iid
       end
 
-      it 'creates a new project issue' do
-        expect(response).to have_http_status(:created)
-      end
+      context 'resolving all discussions in a merge request' do
+        before do
+          post api("/projects/#{project.id}/issues", user),
+               title: 'New Issue',
+               merge_request_to_resolve_discussions_of: merge_request.iid
+        end
 
-      it 'resolves the discussions in a merge request' do
-        discussion.first_note.reload
-
-        expect(discussion.resolved?).to be(true)
+        it_behaves_like 'creating an issue resolving discussions through the API'
       end
 
-      it 'assigns a description to the issue mentioning the merge request' do
-        expect(json_response['description']).to include(merge_request.to_reference)
+      context 'resolving a single discussion' do
+        before do
+          post api("/projects/#{project.id}/issues", user),
+               title: 'New Issue',
+               merge_request_to_resolve_discussions_of: merge_request.iid,
+               discussion_to_resolve: discussion.id
+        end
+
+        it_behaves_like 'creating an issue resolving discussions through the API'
       end
     end
 
@@ -940,23 +1037,29 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe "PUT /projects/:id/issues/:issue_id to update only title" do
+  describe "PUT /projects/:id/issues/:issue_iid to update only title" do
     it "updates a project issue" do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
         title: 'updated title'
       expect(response).to have_http_status(200)
 
       expect(json_response['title']).to eq('updated title')
     end
 
-    it "returns 404 error if issue id not found" do
+    it "returns 404 error if issue iid not found" do
       put api("/projects/#{project.id}/issues/44444", user),
         title: 'updated title'
       expect(response).to have_http_status(404)
     end
 
-    it 'allows special label names' do
+    it "returns 404 error if issue id is used instead of the iid" do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
+          title: 'updated title'
+      expect(response).to have_http_status(404)
+    end
+
+    it 'allows special label names' do
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           title: 'updated title',
           labels: 'label, label?, label&foo, ?, &'
 
@@ -970,40 +1073,40 @@ describe API::Issues, api: true  do
 
     context 'confidential issues' do
       it "returns 403 for non project members" do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
       it "returns 403 for project members with guest role" do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
       it "updates a confidential issue for project members" do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
       it "updates a confidential issue for author" do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
       it "updates a confidential issue for admin" do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
       it 'sets an issue to confidential' do
-        put api("/projects/#{project.id}/issues/#{issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           confidential: true
 
         expect(response).to have_http_status(200)
@@ -1011,7 +1114,7 @@ describe API::Issues, api: true  do
       end
 
       it 'makes a confidential issue public' do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
           confidential: false
 
         expect(response).to have_http_status(200)
@@ -1019,7 +1122,7 @@ describe API::Issues, api: true  do
       end
 
       it 'does not update a confidential issue with wrong confidential flag' do
-        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
           confidential: 'foo'
 
         expect(response).to have_http_status(400)
@@ -1028,7 +1131,7 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
+  describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
     let(:params) do
       {
         title: 'updated title',
@@ -1041,7 +1144,7 @@ describe API::Issues, api: true  do
       allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
       allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
 
-      put api("/projects/#{project.id}/issues/#{issue.id}", user), params
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user), params
 
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -1055,12 +1158,12 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe 'PUT /projects/:id/issues/:issue_id to update labels' do
+  describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
     let!(:label) { create(:label, title: 'dummy', project: project) }
     let!(:label_link) { create(:label_link, label: label, target: issue) }
 
     it 'does not update labels if not present' do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           title: 'updated title'
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([label.title])
@@ -1071,7 +1174,7 @@ describe API::Issues, api: true  do
       label.toggle_subscription(user2, project)
 
       perform_enqueued_jobs do
-        put api("/projects/#{project.id}/issues/#{issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           title: 'updated title', labels: label.title
       end
 
@@ -1079,14 +1182,14 @@ describe API::Issues, api: true  do
     end
 
     it 'removes all labels' do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
 
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([])
     end
 
     it 'updates labels' do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           labels: 'foo,bar'
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to include 'foo'
@@ -1094,7 +1197,7 @@ describe API::Issues, api: true  do
     end
 
     it 'allows special label names' do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
       expect(response.status).to eq(200)
       expect(json_response['labels']).to include 'label:foo'
@@ -1108,7 +1211,7 @@ describe API::Issues, api: true  do
     end
 
     it 'returns 400 if title is too long' do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           title: 'g' * 256
       expect(response).to have_http_status(400)
       expect(json_response['message']['title']).to eq([
@@ -1117,9 +1220,9 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe "PUT /projects/:id/issues/:issue_id to update state and label" do
+  describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
     it "updates a project issue" do
-      put api("/projects/#{project.id}/issues/#{issue.id}", user),
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
         labels: 'label2', state_event: "close"
       expect(response).to have_http_status(200)
 
@@ -1128,7 +1231,7 @@ describe API::Issues, api: true  do
     end
 
     it 'reopens a project isssue' do
-      put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+      put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen'
 
       expect(response).to have_http_status(200)
       expect(json_response['state']).to eq 'reopened'
@@ -1137,7 +1240,7 @@ describe API::Issues, api: true  do
     context 'when an admin or owner makes the request' do
       it 'accepts the update date to be set' do
         update_time = 2.weeks.ago
-        put api("/projects/#{project.id}/issues/#{issue.id}", user),
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
           labels: 'label3', state_event: 'close', updated_at: update_time
 
         expect(response).to have_http_status(200)
@@ -1147,25 +1250,25 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+  describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
     it 'creates a new project issue' do
       due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
 
-      put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date
 
       expect(response).to have_http_status(200)
       expect(json_response['due_date']).to eq(due_date)
     end
   end
 
-  describe "DELETE /projects/:id/issues/:issue_id" do
+  describe "DELETE /projects/:id/issues/:issue_iid" do
     it "rejects a non member from deleting an issue" do
-      delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+      delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
       expect(response).to have_http_status(403)
     end
 
     it "rejects a developer from deleting an issue" do
-      delete api("/projects/#{project.id}/issues/#{issue.id}", author)
+      delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
       expect(response).to have_http_status(403)
     end
 
@@ -1174,9 +1277,9 @@ describe API::Issues, api: true  do
       let(:project)   { create(:empty_project, namespace: owner.namespace) }
 
       it "deletes the issue if an admin requests it" do
-        delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
-        expect(response).to have_http_status(200)
-        expect(json_response['state']).to eq 'opened'
+        delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
+
+        expect(response).to have_http_status(204)
       end
     end
 
@@ -1187,14 +1290,20 @@ describe API::Issues, api: true  do
         expect(response).to have_http_status(404)
       end
     end
+
+    it 'returns 404 when using the issue ID instead of IID' do
+      delete api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe '/projects/:id/issues/:issue_id/move' do
+  describe '/projects/:id/issues/:issue_iid/move' do
     let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
     let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
 
     it 'moves an issue' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+      post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
                to_project_id: target_project.id
 
       expect(response).to have_http_status(201)
@@ -1203,7 +1312,7 @@ describe API::Issues, api: true  do
 
     context 'when source and target projects are the same' do
       it 'returns 400 when trying to move an issue' do
-        post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+        post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
                  to_project_id: project.id
 
         expect(response).to have_http_status(400)
@@ -1213,7 +1322,7 @@ describe API::Issues, api: true  do
 
     context 'when the user does not have the permission to move issues' do
       it 'returns 400 when trying to move an issue' do
-        post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+        post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
                  to_project_id: target_project2.id
 
         expect(response).to have_http_status(400)
@@ -1222,13 +1331,23 @@ describe API::Issues, api: true  do
     end
 
     it 'moves the issue to another namespace if I am admin' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
+      post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
                to_project_id: target_project2.id
 
       expect(response).to have_http_status(201)
       expect(json_response['project_id']).to eq(target_project2.id)
     end
 
+    context 'when using the issue ID instead of iid' do
+      it 'returns 404 when trying to move an issue' do
+        post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+             to_project_id: target_project.id
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 Issue Not Found')
+      end
+    end
+
     context 'when issue does not exist' do
       it 'returns 404 when trying to move an issue' do
         post api("/projects/#{project.id}/issues/123/move", user),
@@ -1241,7 +1360,7 @@ describe API::Issues, api: true  do
 
     context 'when source project does not exist' do
       it 'returns 404 when trying to move an issue' do
-        post api("/projects/123/issues/#{issue.id}/move", user),
+        post api("/projects/123/issues/#{issue.iid}/move", user),
                  to_project_id: target_project.id
 
         expect(response).to have_http_status(404)
@@ -1251,7 +1370,7 @@ describe API::Issues, api: true  do
 
     context 'when target project does not exist' do
       it 'returns 404 when trying to move an issue' do
-        post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+        post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
                  to_project_id: 123
 
         expect(response).to have_http_status(404)
@@ -1259,16 +1378,16 @@ describe API::Issues, api: true  do
     end
   end
 
-  describe 'POST :id/issues/:issue_id/subscribe' do
+  describe 'POST :id/issues/:issue_iid/subscribe' do
     it 'subscribes to an issue' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2)
+      post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
 
       expect(response).to have_http_status(201)
       expect(json_response['subscribed']).to eq(true)
     end
 
     it 'returns 304 if already subscribed' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+      post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
 
       expect(response).to have_http_status(304)
     end
@@ -1279,8 +1398,14 @@ describe API::Issues, api: true  do
       expect(response).to have_http_status(404)
     end
 
+    it 'returns 404 if the issue ID is used instead of the iid' do
+      post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     it 'returns 404 if the issue is confidential' do
-      post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member)
+      post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
 
       expect(response).to have_http_status(404)
     end
@@ -1288,14 +1413,14 @@ describe API::Issues, api: true  do
 
   describe 'POST :id/issues/:issue_id/unsubscribe' do
     it 'unsubscribes from an issue' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+      post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
 
       expect(response).to have_http_status(201)
       expect(json_response['subscribed']).to eq(false)
     end
 
     it 'returns 304 if not subscribed' do
-      post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2)
+      post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
 
       expect(response).to have_http_status(304)
     end
@@ -1306,8 +1431,14 @@ describe API::Issues, api: true  do
       expect(response).to have_http_status(404)
     end
 
+    it 'returns 404 if using the issue ID instead of iid' do
+      post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     it 'returns 404 if the issue is confidential' do
-      post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member)
+      post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
 
       expect(response).to have_http_status(404)
     end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9450701064b5f4c1a668fbe3322b29d96bb223f2
--- /dev/null
+++ b/spec/requests/api/jobs_spec.rb
@@ -0,0 +1,480 @@
+require 'spec_helper'
+
+describe API::Jobs, api: true do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:api_user) { user }
+  let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
+  let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+  let(:reporter) { create(:project_member, :reporter, project: project) }
+  let(:guest) { create(:project_member, :guest, project: project) }
+  let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+  let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+  describe 'GET /projects/:id/jobs' do
+    let(:query) { Hash.new }
+
+    before do
+      get api("/projects/#{project.id}/jobs", api_user), query
+    end
+
+    context 'authorized user' do
+      it 'returns project jobs' do
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+      end
+
+      it 'returns correct values' do
+        expect(json_response).not_to be_empty
+        expect(json_response.first['commit']['id']).to eq project.commit.id
+      end
+
+      it 'returns pipeline data' do
+        json_build = json_response.first
+
+        expect(json_build['pipeline']).not_to be_empty
+        expect(json_build['pipeline']['id']).to eq build.pipeline.id
+        expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+        expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+        expect(json_build['pipeline']['status']).to eq build.pipeline.status
+      end
+
+      context 'filter project with one scope element' do
+        let(:query) { { 'scope' => 'pending' } }
+
+        it do
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+        end
+      end
+
+      context 'filter project with array of scope elements' do
+        let(:query) { { scope: %w(pending running) } }
+
+        it do
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+        end
+      end
+
+      context 'respond 400 when scope contains invalid state' do
+        let(:query) { { scope: %w(unknown running) } }
+
+        it { expect(response).to have_http_status(400) }
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not return project builds' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
+    let(:query) { Hash.new }
+
+    before do
+      get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+    end
+
+    context 'authorized user' do
+      it 'returns pipeline jobs' do
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+      end
+
+      it 'returns correct values' do
+        expect(json_response).not_to be_empty
+        expect(json_response.first['commit']['id']).to eq project.commit.id
+      end
+
+      it 'returns pipeline data' do
+        json_build = json_response.first
+
+        expect(json_build['pipeline']).not_to be_empty
+        expect(json_build['pipeline']['id']).to eq build.pipeline.id
+        expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+        expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+        expect(json_build['pipeline']['status']).to eq build.pipeline.status
+      end
+
+      context 'filter jobs with one scope element' do
+        let(:query) { { 'scope' => 'pending' } }
+
+        it do
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+        end
+      end
+
+      context 'filter jobs with array of scope elements' do
+        let(:query) { { scope: %w(pending running) } }
+
+        it do
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+        end
+      end
+
+      context 'respond 400 when scope contains invalid state' do
+        let(:query) { { scope: %w(unknown running) } }
+
+        it { expect(response).to have_http_status(400) }
+      end
+
+      context 'jobs in different pipelines' do
+        let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+        let!(:build2) { create(:ci_build, pipeline: pipeline2) }
+
+        it 'excludes jobs from other pipelines' do
+          json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not return jobs' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/jobs/:job_id' do
+    before do
+      get api("/projects/#{project.id}/jobs/#{build.id}", api_user)
+    end
+
+    context 'authorized user' do
+      it 'returns specific job data' do
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq('test')
+      end
+
+      it 'returns pipeline data' do
+        json_build = json_response
+        expect(json_build['pipeline']).not_to be_empty
+        expect(json_build['pipeline']['id']).to eq build.pipeline.id
+        expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+        expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+        expect(json_build['pipeline']['status']).to eq build.pipeline.status
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not return specific job data' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/jobs/:job_id/artifacts' do
+    before do
+      get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user)
+    end
+
+    context 'job with artifacts' do
+      let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+      context 'authorized user' do
+        let(:download_headers) do
+          { 'Content-Transfer-Encoding' => 'binary',
+            'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+        end
+
+        it 'returns specific job artifacts' do
+          expect(response).to have_http_status(200)
+          expect(response.headers).to include(download_headers)
+          expect(response.body).to match_file(build.artifacts_file.file.file)
+        end
+      end
+
+      context 'unauthorized user' do
+        let(:api_user) { nil }
+
+        it 'does not return specific job artifacts' do
+          expect(response).to have_http_status(401)
+        end
+      end
+    end
+
+    it 'does not return job artifacts if not uploaded' do
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+    let(:api_user) { reporter.user }
+    let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+    before do
+      build.success
+    end
+
+    def get_for_ref(ref = pipeline.ref, job = build.name)
+      get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job
+    end
+
+    context 'when not logged in' do
+      let(:api_user) { nil }
+
+      before do
+        get_for_ref
+      end
+
+      it 'gives 401' do
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when logging as guest' do
+      let(:api_user) { guest.user }
+
+      before do
+        get_for_ref
+      end
+
+      it 'gives 403' do
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'non-existing job' do
+      shared_examples 'not found' do
+        it { expect(response).to have_http_status(:not_found) }
+      end
+
+      context 'has no such ref' do
+        before do
+          get_for_ref('TAIL')
+        end
+
+        it_behaves_like 'not found'
+      end
+
+      context 'has no such job' do
+        before do
+          get_for_ref(pipeline.ref, 'NOBUILD')
+        end
+
+        it_behaves_like 'not found'
+      end
+    end
+
+    context 'find proper job' do
+      shared_examples 'a valid file' do
+        let(:download_headers) do
+          { 'Content-Transfer-Encoding' => 'binary',
+            'Content-Disposition' =>
+              "attachment; filename=#{build.artifacts_file.filename}" }
+        end
+
+        it { expect(response).to have_http_status(200) }
+        it { expect(response.headers).to include(download_headers) }
+      end
+
+      context 'with regular branch' do
+        before do
+          pipeline.reload
+          pipeline.update(ref: 'master',
+                          sha: project.commit('master').sha)
+
+          get_for_ref('master')
+        end
+
+        it_behaves_like 'a valid file'
+      end
+
+      context 'with branch name containing slash' do
+        before do
+          pipeline.reload
+          pipeline.update(ref: 'improve/awesome',
+                          sha: project.commit('improve/awesome').sha)
+        end
+
+        before do
+          get_for_ref('improve/awesome')
+        end
+
+        it_behaves_like 'a valid file'
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/jobs/:job_id/trace' do
+    let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+    before do
+      get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user)
+    end
+
+    context 'authorized user' do
+      it 'returns specific job trace' do
+        expect(response).to have_http_status(200)
+        expect(response.body).to eq(build.trace)
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not return specific job trace' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/jobs/:job_id/cancel' do
+    before do
+      post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user)
+    end
+
+    context 'authorized user' do
+      context 'user with :update_build persmission' do
+        it 'cancels running or pending job' do
+          expect(response).to have_http_status(201)
+          expect(project.builds.first.status).to eq('canceled')
+        end
+      end
+
+      context 'user without :update_build permission' do
+        let(:api_user) { reporter.user }
+
+        it 'does not cancel job' do
+          expect(response).to have_http_status(403)
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not cancel job' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/jobs/:job_id/retry' do
+    let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+    before do
+      post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user)
+    end
+
+    context 'authorized user' do
+      context 'user with :update_build permission' do
+        it 'retries non-running job' do
+          expect(response).to have_http_status(201)
+          expect(project.builds.first.status).to eq('canceled')
+          expect(json_response['status']).to eq('pending')
+        end
+      end
+
+      context 'user without :update_build permission' do
+        let(:api_user) { reporter.user }
+
+        it 'does not retry job' do
+          expect(response).to have_http_status(403)
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      let(:api_user) { nil }
+
+      it 'does not retry job' do
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/jobs/:job_id/erase' do
+    before do
+      post api("/projects/#{project.id}/jobs/#{build.id}/erase", user)
+    end
+
+    context 'job is erasable' do
+      let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+
+      it 'erases job content' do
+        expect(response).to have_http_status(201)
+        expect(build.trace).to be_empty
+        expect(build.artifacts_file.exists?).to be_falsy
+        expect(build.artifacts_metadata.exists?).to be_falsy
+      end
+
+      it 'updates job' do
+        build.reload
+        expect(build.erased_at).to be_truthy
+        expect(build.erased_by).to eq(user)
+      end
+    end
+
+    context 'job is not erasable' do
+      let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+
+      it 'responds with forbidden' do
+        expect(response).to have_http_status(403)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do
+    before do
+      post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user)
+    end
+
+    context 'artifacts did not expire' do
+      let(:build) do
+        create(:ci_build, :trace, :artifacts, :success,
+               project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+      end
+
+      it 'keeps artifacts' do
+        expect(response).to have_http_status(200)
+        expect(build.reload.artifacts_expire_at).to be_nil
+      end
+    end
+
+    context 'no artifacts' do
+      let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+      it 'responds with not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/jobs/:job_id/play' do
+    before do
+      post api("/projects/#{project.id}/jobs/#{build.id}/play", user)
+    end
+
+    context 'on an playable job' do
+      let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+      it 'plays the job' do
+        expect(response).to have_http_status(200)
+        expect(json_response['user']['id']).to eq(user.id)
+        expect(json_response['id']).to eq(build.id)
+      end
+    end
+
+    context 'on a non-playable job' do
+      it 'returns a status code 400, Bad Request' do
+        expect(response).to have_http_status 400
+        expect(response.body).to match("Unplayable Job")
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 566d11bba57caadc22562823a7bff819405ab2c5..a1adaba7b9822db3dc2b889bdcf506904b26eef0 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -21,11 +21,11 @@ describe API::Labels, api: true  do
       create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
       create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
 
-      expected_keys = [
-        'id', 'name', 'color', 'description',
-        'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
-        'subscribed', 'priority'
-      ]
+      expected_keys = %w(
+        id name color description
+        open_issues_count closed_issues_count open_merge_requests_count
+        subscribed priority
+      )
 
       get api("/projects/#{project.id}/labels", user)
 
@@ -175,9 +175,10 @@ describe API::Labels, api: true  do
   end
 
   describe 'DELETE /projects/:id/labels' do
-    it 'returns 200 for existing label' do
+    it 'returns 204 for existing label' do
       delete api("/projects/#{project.id}/labels", user), name: 'label1'
-      expect(response).to have_http_status(200)
+
+      expect(response).to have_http_status(204)
     end
 
     it 'returns 404 for non existing label' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 31166b50033d850542d1734c004a75638ee3daed..2d37d026a397a9cac08c2732721087ea40760ca5 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -173,11 +173,11 @@ describe API::Members, api: true  do
         expect(response).to have_http_status(400)
       end
 
-      it 'returns 422 when access_level is not valid' do
+      it 'returns 400  when access_level is not valid' do
         post api("/#{source_type.pluralize}/#{source.id}/members", master),
              user_id: stranger.id, access_level: 1234
 
-        expect(response).to have_http_status(422)
+        expect(response).to have_http_status(400)
       end
     end
   end
@@ -230,11 +230,11 @@ describe API::Members, api: true  do
         expect(response).to have_http_status(400)
       end
 
-      it 'returns 422 when access level is not valid' do
+      it 'returns 400  when access level is not valid' do
         put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
             access_level: 1234
 
-        expect(response).to have_http_status(422)
+        expect(response).to have_http_status(400)
       end
     end
   end
@@ -263,18 +263,18 @@ describe API::Members, api: true  do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
 
-            expect(response).to have_http_status(200)
+            expect(response).to have_http_status(204)
           end.to change { source.members.count }.by(-1)
         end
       end
 
       context 'when authenticated as a master/owner' do
         context 'and member is a requester' do
-          it "returns #{source_type == 'project' ? 200 : 404}" do
+          it 'returns 404' do
             expect do
               delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
 
-              expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+              expect(response).to have_http_status(404)
             end.not_to change { source.requesters.count }
           end
         end
@@ -283,15 +283,15 @@ describe API::Members, api: true  do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
 
-            expect(response).to have_http_status(200)
+            expect(response).to have_http_status(204)
           end.to change { source.members.count }.by(-1)
         end
       end
 
-      it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+      it 'returns 404 if member does not exist' do
         delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
 
-        expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+        expect(response).to have_http_status(404)
       end
     end
   end
@@ -342,7 +342,7 @@ describe API::Members, api: true  do
         post api("/projects/#{project.id}/members", master),
              user_id: stranger.id, access_level: Member::OWNER
 
-        expect(response).to have_http_status(422)
+        expect(response).to have_http_status(400)
       end.to change { project.members.count }.by(0)
     end
   end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 1d02e827183393734cf2814e4d3656a56bf71427..79f3151ba52ff32a74fe6647be863c35db805946 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -13,9 +13,9 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true  do
     project.team << [user, :master]
   end
 
-  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+  describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
     it 'returns 200 for a valid merge request' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user)
       merge_request_diff = merge_request.merge_request_diffs.first
 
       expect(response.status).to eq 200
@@ -26,16 +26,22 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true  do
       expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
     end
 
-    it 'returns a 404 when merge_request_id not found' do
+    it 'returns a 404 when merge_request id is used instead of the iid' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it 'returns a 404 when merge_request_iid not found' do
       get api("/projects/#{project.id}/merge_requests/999/versions", user)
       expect(response).to have_http_status(404)
     end
   end
 
-  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+  describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do
+    let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
     it 'returns a 200 for a valid merge request' do
-      merge_request_diff = merge_request.merge_request_diffs.first
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}", user)
 
       expect(response.status).to eq 200
       expect(json_response['id']).to eq(merge_request_diff.id)
@@ -43,8 +49,18 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true  do
       expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
     end
 
-    it 'returns a 404 when merge_request_id not found' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+    it 'returns a 404 when merge_request id is used instead of the iid' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it 'returns a 404 when merge_request version_id is not found' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it 'returns a 404 when merge_request_iid is not found' do
+      get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user)
       expect(response).to have_http_status(404)
     end
   end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index c125df8b90b5ba7d4f01a1c672a0a1e52c0ad47b..9aba1d7561263a56e55668c7063ab23964d8e967 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -93,6 +93,13 @@ describe API::MergeRequests, api: true  do
         expect(json_response.first['id']).to eq merge_request_closed.id
       end
 
+      it 'matches V4 response schema' do
+        get api("/projects/#{project.id}/merge_requests", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to match_response_schema('public_api/v4/merge_requests')
+      end
+
       context "with ordering" do
         before do
           @mr_later = mr_with_later_created_and_updated_at_time
@@ -146,9 +153,9 @@ describe API::MergeRequests, api: true  do
     end
   end
 
-  describe "GET /projects/:id/merge_requests/:merge_request_id" do
+  describe "GET /projects/:id/merge_requests/:merge_request_iid" do
     it 'exposes known attributes' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['id']).to eq(merge_request.id)
@@ -170,14 +177,14 @@ describe API::MergeRequests, api: true  do
       expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
       expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
       expect(json_response['work_in_progress']).to be_falsy
-      expect(json_response['merge_when_build_succeeds']).to be_falsy
+      expect(json_response['merge_when_pipeline_succeeds']).to be_falsy
       expect(json_response['merge_status']).to eq('can_be_merged')
       expect(json_response['should_close_merge_request']).to be_falsy
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
     it "returns merge_request" do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(merge_request.title)
       expect(json_response['iid']).to eq(merge_request.iid)
@@ -187,25 +194,31 @@ describe API::MergeRequests, api: true  do
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
-    it "returns a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_iid not found" do
       get api("/projects/#{project.id}/merge_requests/999", user)
       expect(response).to have_http_status(404)
     end
 
+    it "returns a 404 error if merge_request `id` is used instead of iid" do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     context 'Work in Progress' do
       let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
 
       it "returns merge_request" do
-        get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+        get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
         expect(response).to have_http_status(200)
         expect(json_response['work_in_progress']).to eq(true)
       end
     end
   end
 
-  describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
+  describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
     it 'returns a 200 when merge request is valid' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
       commit = merge_request.commits.first
 
       expect(response.status).to eq 200
@@ -216,24 +229,36 @@ describe API::MergeRequests, api: true  do
       expect(json_response.first['title']).to eq(commit.title)
     end
 
-    it 'returns a 404 when merge_request_id not found' do
+    it 'returns a 404 when merge_request_iid not found' do
       get api("/projects/#{project.id}/merge_requests/999/commits", user)
       expect(response).to have_http_status(404)
     end
+
+    it 'returns a 404 when merge_request id is used instead of iid' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+  describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do
     it 'returns the change information of the merge_request' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
 
       expect(response.status).to eq 200
       expect(json_response['changes'].size).to eq(merge_request.diffs.size)
     end
 
-    it 'returns a 404 when merge_request_id not found' do
+    it 'returns a 404 when merge_request_iid not found' do
       get api("/projects/#{project.id}/merge_requests/999/changes", user)
       expect(response).to have_http_status(404)
     end
+
+    it 'returns a 404 when merge_request id is used instead of iid' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+
+      expect(response).to have_http_status(404)
+    end
   end
 
   describe "POST /projects/:id/merge_requests" do
@@ -250,7 +275,7 @@ describe API::MergeRequests, api: true  do
 
         expect(response).to have_http_status(201)
         expect(json_response['title']).to eq('Test merge_request')
-        expect(json_response['labels']).to eq(['label', 'label2'])
+        expect(json_response['labels']).to eq(%w(label label2))
         expect(json_response['milestone']['id']).to eq(milestone.id)
         expect(json_response['force_remove_source_branch']).to be_truthy
       end
@@ -393,7 +418,7 @@ describe API::MergeRequests, api: true  do
     end
   end
 
-  describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+  describe "DELETE /projects/:id/merge_requests/:merge_request_iid" do
     context "when the user is developer" do
       let(:developer) { create(:user) }
 
@@ -402,25 +427,37 @@ describe API::MergeRequests, api: true  do
       end
 
       it "denies the deletion of the merge request" do
-        delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+        delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when the user is project owner" do
       it "destroys the merge request owners can destroy" do
+        delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+        expect(response).to have_http_status(204)
+      end
+
+      it "returns 404 for an invalid merge request IID" do
+        delete api("/projects/#{project.id}/merge_requests/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "returns 404 if the merge request id is used instead of iid" do
         delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(404)
       end
     end
   end
 
-  describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+  describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do
     let(:pipeline) { create(:ci_pipeline_without_jobs) }
 
     it "returns merge_request in case of success" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
 
       expect(response).to have_http_status(200)
     end
@@ -429,7 +466,7 @@ describe API::MergeRequests, api: true  do
       allow_any_instance_of(MergeRequest).
         to receive(:can_be_merged?).and_return(false)
 
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
 
       expect(response).to have_http_status(406)
       expect(json_response['message']).to eq('Branch cannot be merged')
@@ -437,14 +474,14 @@ describe API::MergeRequests, api: true  do
 
     it "returns 405 if merge_request is not open" do
       merge_request.close
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
 
     it "returns 405 if merge_request is a work in progress" do
       merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
@@ -452,7 +489,7 @@ describe API::MergeRequests, api: true  do
     it 'returns 405 if the build failed for a merge request that requires success' do
       allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
 
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
 
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('405 Method Not Allowed')
@@ -461,20 +498,20 @@ describe API::MergeRequests, api: true  do
     it "returns 401 if user has no permissions to merge" do
       user2 = create(:user)
       project.team << [user2, :reporter]
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2)
       expect(response).to have_http_status(401)
       expect(json_response['message']).to eq('401 Unauthorized')
     end
 
     it "returns 409 if the SHA parameter doesn't match" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse
 
       expect(response).to have_http_status(409)
       expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
     end
 
     it "succeeds if the SHA parameter matches" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha
 
       expect(response).to have_http_status(200)
     end
@@ -483,18 +520,30 @@ describe API::MergeRequests, api: true  do
       allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
       allow(pipeline).to receive(:active?).and_return(true)
 
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
 
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq('Test')
-      expect(json_response['merge_when_build_succeeds']).to eq(true)
+      expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
+    end
+
+    it "returns 404 for an invalid merge request IID" do
+      put api("/projects/#{project.id}/merge_requests/12345/merge", user)
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 404 if the merge request id is used instead of iid" do
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+      expect(response).to have_http_status(404)
     end
   end
 
-  describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+  describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
     context "to close a MR" do
       it "returns merge_request" do
-        put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+        put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close"
 
         expect(response).to have_http_status(200)
         expect(json_response['state']).to eq('closed')
@@ -502,38 +551,38 @@ describe API::MergeRequests, api: true  do
     end
 
     it "updates title and returns merge_request" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title"
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq('New title')
     end
 
     it "updates description and returns merge_request" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description"
       expect(response).to have_http_status(200)
       expect(json_response['description']).to eq('New description')
     end
 
     it "updates milestone_id and returns merge_request" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id
       expect(response).to have_http_status(200)
       expect(json_response['milestone']['id']).to eq(milestone.id)
     end
 
     it "returns merge_request with renamed target_branch" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki"
       expect(response).to have_http_status(200)
       expect(json_response['target_branch']).to eq('wiki')
     end
 
     it "returns merge_request that removes the source branch" do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true
 
       expect(response).to have_http_status(200)
       expect(json_response['force_remove_source_branch']).to be_truthy
     end
 
     it 'allows special label names' do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
         title: 'new issue',
         labels: 'label, label?, label&foo, ?, &'
 
@@ -546,7 +595,7 @@ describe API::MergeRequests, api: true  do
     end
 
     it 'does not update state when title is empty' do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil
 
       merge_request.reload
       expect(response).to have_http_status(400)
@@ -554,19 +603,31 @@ describe API::MergeRequests, api: true  do
     end
 
     it 'does not update state when target_branch is empty' do
-      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil
 
       merge_request.reload
       expect(response).to have_http_status(400)
       expect(merge_request.state).to eq('opened')
     end
+
+    it "returns 404 for an invalid merge request IID" do
+      put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close"
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 404 if the merge request id is used instead of iid" do
+      put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+  describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do
     it "returns comment" do
       original_count = merge_request.notes.size
 
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment"
 
       expect(response).to have_http_status(201)
       expect(json_response['note']).to eq('My comment')
@@ -576,23 +637,29 @@ describe API::MergeRequests, api: true  do
     end
 
     it "returns 400 if note is missing" do
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
       expect(response).to have_http_status(400)
     end
 
-    it "returns 404 if note is attached to non existent merge request" do
+    it "returns 404 if merge request iid is invalid" do
       post api("/projects/#{project.id}/merge_requests/404/comments", user),
         note: 'My comment'
       expect(response).to have_http_status(404)
     end
+
+    it "returns 404 if merge request id is used instead of iid" do
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user),
+        note: 'My comment'
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe "GET :id/merge_requests/:merge_request_id/comments" do
+  describe "GET :id/merge_requests/:merge_request_iid/comments" do
     let!(:note)  { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
     let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
 
     it "returns merge_request comments ordered by created_at" do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -603,20 +670,25 @@ describe API::MergeRequests, api: true  do
       expect(json_response.last['note']).to eq("another comment on a MR")
     end
 
-    it "returns a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_iid is invalid" do
       get api("/projects/#{project.id}/merge_requests/999/comments", user)
       expect(response).to have_http_status(404)
     end
+
+    it "returns a 404 error if merge_request id is used instead of iid" do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+  describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do
     it 'returns the issue that will be closed on merge' do
       issue = create(:issue, project: project)
       mr = merge_request.tap do |mr|
         mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
       end
 
-      get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+      get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -626,7 +698,7 @@ describe API::MergeRequests, api: true  do
     end
 
     it 'returns an empty array when there are no issues to be closed' do
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -640,7 +712,7 @@ describe API::MergeRequests, api: true  do
       merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
       merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
 
-      get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+      get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -656,22 +728,34 @@ describe API::MergeRequests, api: true  do
       guest = create(:user)
       project.team << [guest, :guest]
 
-      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest)
 
       expect(response).to have_http_status(403)
     end
+
+    it "returns 404 for an invalid merge request IID" do
+      get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user)
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 404 if the merge request id is used instead of iid" do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+      expect(response).to have_http_status(404)
+    end
   end
 
-  describe 'POST :id/merge_requests/:merge_request_id/subscribe' do
+  describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do
     it 'subscribes to a merge request' do
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin)
 
       expect(response).to have_http_status(201)
       expect(json_response['subscribed']).to eq(true)
     end
 
     it 'returns 304 if already subscribed' do
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user)
 
       expect(response).to have_http_status(304)
     end
@@ -682,26 +766,32 @@ describe API::MergeRequests, api: true  do
       expect(response).to have_http_status(404)
     end
 
+    it 'returns 404 if the merge request id is used instead of iid' do
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     it 'returns 403 if user has no access to read code' do
       guest = create(:user)
       project.team << [guest, :guest]
 
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
 
       expect(response).to have_http_status(403)
     end
   end
 
-  describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do
+  describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do
     it 'unsubscribes from a merge request' do
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user)
 
       expect(response).to have_http_status(201)
       expect(json_response['subscribed']).to eq(false)
     end
 
     it 'returns 304 if not subscribed' do
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
 
       expect(response).to have_http_status(304)
     end
@@ -712,11 +802,17 @@ describe API::MergeRequests, api: true  do
       expect(response).to have_http_status(404)
     end
 
+    it 'returns 404 if the merge request id is used instead of iid' do
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
+
+      expect(response).to have_http_status(404)
+    end
+
     it 'returns 403 if user has no access to read code' do
       guest = create(:user)
       project.team << [guest, :guest]
 
-      post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest)
+      post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest)
 
       expect(response).to have_http_status(403)
     end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 418bf5a507cfa122efbcb387c94dc195a0a7a6fc..7fb728fed6fb6e7387ceccc3da482b0cd1f9a608 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -4,8 +4,8 @@ describe API::Milestones, api: true  do
   include ApiHelpers
   let(:user) { create(:user) }
   let!(:project) { create(:empty_project, namespace: user.namespace ) }
-  let!(:closed_milestone) { create(:closed_milestone, project: project) }
-  let!(:milestone) { create(:milestone, project: project) }
+  let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
+  let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
 
   before { project.team << [user, :developer] }
 
@@ -45,8 +45,37 @@ describe API::Milestones, api: true  do
       expect(json_response.first['id']).to eq(closed_milestone.id)
     end
 
-    it 'returns a project milestone by iid' do
-      get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
+    it 'returns an array of milestones specified by iids' do
+      other_milestone = create(:milestone, project: project)
+
+      get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid]
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(2)
+      expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
+    end
+
+    it 'does not return any milestone if none found' do
+      get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ]
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(0)
+    end
+  end
+
+  describe 'GET /projects/:id/milestones/:milestone_id' do
+    it 'returns a project milestone by id' do
+      get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['title']).to eq(milestone.title)
+      expect(json_response['iid']).to eq(milestone.iid)
+    end
+
+    it 'returns a project milestone by iids array' do
+      get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user)
 
       expect(response.status).to eq 200
       expect(response).to include_pagination_headers
@@ -56,21 +85,22 @@ describe API::Milestones, api: true  do
       expect(json_response.first['id']).to eq closed_milestone.id
     end
 
-    it 'returns a project milestone by iid array' do
-      get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+    it 'returns a project milestone by searching for title' do
+      get api("/projects/#{project.id}/milestones", user), search: 'version2'
 
       expect(response).to have_http_status(200)
-      expect(json_response.size).to eq(2)
+      expect(response).to include_pagination_headers
+      expect(json_response.size).to eq(1)
       expect(json_response.first['title']).to eq milestone.title
       expect(json_response.first['id']).to eq milestone.id
     end
 
-    it 'returns a project milestone by iid array' do
-      get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+    it 'returns a project milestones by searching for description' do
+      get api("/projects/#{project.id}/milestones", user), search: 'open'
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
-      expect(json_response.size).to eq(2)
+      expect(json_response.size).to eq(1)
       expect(json_response.first['title']).to eq milestone.title
       expect(json_response.first['id']).to eq milestone.id
     end
@@ -197,6 +227,13 @@ describe API::Milestones, api: true  do
       expect(json_response.first['milestone']['title']).to eq(milestone.title)
     end
 
+    it 'matches V4 response schema for a list of issues' do
+      get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+      expect(response).to have_http_status(200)
+      expect(response).to match_response_schema('public_api/v4/issues')
+    end
+
     it 'returns a 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
 
@@ -206,8 +243,8 @@ describe API::Milestones, api: true  do
     describe 'confidential issues' do
       let(:public_project) { create(:empty_project, :public) }
       let(:milestone) { create(:milestone, project: public_project) }
-      let(:issue) { create(:issue, project: public_project) }
-      let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+      let(:issue) { create(:issue, project: public_project, position: 2) }
+      let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) }
 
       before do
         public_project.team << [user, :developer]
@@ -246,11 +283,24 @@ describe API::Milestones, api: true  do
         expect(json_response.size).to eq(1)
         expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
       end
+
+      it 'returns issues ordered by position asc' do
+        get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(2)
+        expect(json_response.first['id']).to eq(confidential_issue.id)
+        expect(json_response.second['id']).to eq(issue.id)
+      end
     end
   end
 
   describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
-    let(:merge_request) { create(:merge_request, source_project: project) }
+    let(:merge_request) { create(:merge_request, source_project: project, position: 2) }
+    let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) }
+
     before do
       milestone.merge_requests << merge_request
     end
@@ -283,5 +333,18 @@ describe API::Milestones, api: true  do
 
       expect(response).to have_http_status(401)
     end
+
+    it 'returns merge_requests ordered by position asc' do
+      milestone.merge_requests << another_merge_request
+
+      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(2)
+      expect(json_response.first['id']).to eq(another_merge_request.id)
+      expect(json_response.second['id']).to eq(merge_request.id)
+    end
   end
 end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 3cca4468be71d43ea85b28cd62263ca96898c72a..347f8f6fa3bf0f0b9eda893c53e256520279483c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -225,11 +225,11 @@ describe API::Notes, api: true  do
       context 'when the user is posting an award emoji on an issue created by someone else' do
         let(:issue2) { create(:issue, project: project) }
 
-        it 'returns an award emoji' do
+        it 'creates a new issue note' do
           post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
 
           expect(response).to have_http_status(201)
-          expect(json_response['awardable_id']).to eq issue2.id
+          expect(json_response['body']).to eq(':+1:')
         end
       end
 
@@ -373,7 +373,7 @@ describe API::Notes, api: true  do
         delete api("/projects/#{project.id}/issues/#{issue.id}/"\
                    "notes/#{issue_note.id}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
         # Check if note is really deleted
         delete api("/projects/#{project.id}/issues/#{issue.id}/"\
                    "notes/#{issue_note.id}", user)
@@ -392,7 +392,7 @@ describe API::Notes, api: true  do
         delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                    "notes/#{snippet_note.id}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
         # Check if note is really deleted
         delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                    "notes/#{snippet_note.id}", user)
@@ -412,7 +412,7 @@ describe API::Notes, api: true  do
         delete api("/projects/#{project.id}/merge_requests/"\
                    "#{merge_request.id}/notes/#{merge_request_note.id}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
         # Check if note is really deleted
         delete api("/projects/#{project.id}/merge_requests/"\
                    "#{merge_request.id}/notes/#{merge_request_note.id}", user)
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 7e2cc50e5917b63fefee049fda8b2b4e1fac4662..367225df717fe7d411b81687b2614439d5050e3d 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -29,5 +29,27 @@ describe API::API, api: true  do
         expect(json_response['access_token']).not_to be_nil
       end
     end
+
+    context "when user is blocked" do
+      it "does not create an access token" do
+        user = create(:user)
+        user.block
+
+        request_oauth_token(user)
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when user is ldap_blocked" do
+      it "does not create an access token" do
+        user = create(:user)
+        user.ldap_block
+
+        request_oauth_token(user)
+
+        expect(response).to have_http_status(401)
+      end
+    end
   end
 end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 98d004b572e2dc2afd104d57ef1935d182d35076..51af999b45545bca084719a1d1f4a6b6e931d58f 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -24,6 +24,7 @@ describe API::Pipelines, api: true do
         expect(json_response).to be_an Array
         expect(json_response.first['sha']).to match /\A\h{40}\z/
         expect(json_response.first['id']).to eq pipeline.id
+        expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status])
       end
     end
 
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 20c76bd2c057611373e297336070957223941543..b1f8c249092484491e0319d7a993f17718f90987 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -33,7 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
         expect(json_response.first['merge_requests_events']).to eq(true)
         expect(json_response.first['tag_push_events']).to eq(true)
         expect(json_response.first['note_events']).to eq(true)
-        expect(json_response.first['build_events']).to eq(true)
+        expect(json_response.first['job_events']).to eq(true)
         expect(json_response.first['pipeline_events']).to eq(true)
         expect(json_response.first['wiki_page_events']).to eq(true)
         expect(json_response.first['enable_ssl_verification']).to eq(true)
@@ -59,7 +59,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
         expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
         expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
         expect(json_response['note_events']).to eq(hook.note_events)
-        expect(json_response['build_events']).to eq(hook.build_events)
+        expect(json_response['job_events']).to eq(hook.build_events)
         expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
         expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
         expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -98,7 +98,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
       expect(json_response['merge_requests_events']).to eq(false)
       expect(json_response['tag_push_events']).to eq(false)
       expect(json_response['note_events']).to eq(false)
-      expect(json_response['build_events']).to eq(false)
+      expect(json_response['job_events']).to eq(false)
       expect(json_response['pipeline_events']).to eq(false)
       expect(json_response['wiki_page_events']).to eq(true)
       expect(json_response['enable_ssl_verification']).to eq(true)
@@ -144,7 +144,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
       expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
       expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
       expect(json_response['note_events']).to eq(hook.note_events)
-      expect(json_response['build_events']).to eq(hook.build_events)
+      expect(json_response['job_events']).to eq(hook.build_events)
       expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
       expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
       expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -183,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
     it "deletes hook from project" do
       expect do
         delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
-      end.to change {project.hooks.count}.by(-1)
-      expect(response).to have_http_status(200)
-    end
 
-    it "returns success when deleting hook" do
-      delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
-      expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
+      end.to change {project.hooks.count}.by(-1)
     end
 
     it "returns a 404 error when deleting non existent hook" do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index da9df56401b02b1841fe218b07c1200c6ef33618..9e88c19b0bcd0c596808913c47778ca8e1f70aae 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -44,7 +44,7 @@ describe API::ProjectSnippets, api: true do
         title: 'Test Title',
         file_name: 'test.rb',
         code: 'puts "hello world"',
-        visibility_level: Snippet::PUBLIC
+        visibility: 'public'
       }
     end
 
@@ -56,7 +56,7 @@ describe API::ProjectSnippets, api: true do
       expect(snippet.content).to eq(params[:code])
       expect(snippet.title).to eq(params[:title])
       expect(snippet.file_name).to eq(params[:file_name])
-      expect(snippet.visibility_level).to eq(params[:visibility_level])
+      expect(snippet.visibility_level).to eq(Snippet::PUBLIC)
     end
 
     it 'returns 400 for missing parameters' do
@@ -80,14 +80,14 @@ describe API::ProjectSnippets, api: true do
 
       context 'when the snippet is private' do
         it 'creates the snippet' do
-          expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+          expect { create_snippet(project, visibility: 'private') }.
             to change { Snippet.count }.by(1)
         end
       end
 
       context 'when the snippet is public' do
-        it 'rejects the shippet' do
-          expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+        it 'rejects the snippet' do
+          expect { create_snippet(project, visibility: 'public') }.
             not_to change { Snippet.count }
 
           expect(response).to have_http_status(400)
@@ -95,7 +95,7 @@ describe API::ProjectSnippets, api: true do
         end
 
         it 'creates a spam log' do
-          expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+          expect { create_snippet(project, visibility: 'public') }.
             to change { SpamLog.count }.by(1)
         end
       end
@@ -165,7 +165,7 @@ describe API::ProjectSnippets, api: true do
         let(:visibility_level) { Snippet::PRIVATE }
 
         it 'rejects the snippet' do
-          expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+          expect { update_snippet(title: 'Foo', visibility: 'public') }.
             not_to change { snippet.reload.title }
 
           expect(response).to have_http_status(400)
@@ -173,7 +173,7 @@ describe API::ProjectSnippets, api: true do
         end
 
         it 'creates a spam log' do
-          expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+          expect { update_snippet(title: 'Foo', visibility: 'public') }.
             to change { SpamLog.count }.by(1)
         end
       end
@@ -189,7 +189,7 @@ describe API::ProjectSnippets, api: true do
 
       delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
 
-      expect(response).to have_http_status(200)
+      expect(response).to have_http_status(204)
     end
 
     it 'returns 404 for invalid snippet id' do
@@ -212,7 +212,7 @@ describe API::ProjectSnippets, api: true do
     end
 
     it 'returns 404 for invalid snippet id' do
-      delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
+      get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin)
 
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Snippet Not Found')
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 4e90aae927955c6f840dc0981938a2ee9efa7436..c481b7e72b1e031fab5b69664a7ac3621a1bccba 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 require 'spec_helper'
 
-describe API::Projects, api: true  do
-  include ApiHelpers
+describe API::Projects, :api  do
   include Gitlab::CurrentSettings
+
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
   let(:user3) { create(:user) }
@@ -43,9 +43,10 @@ describe API::Projects, api: true  do
   describe 'GET /projects' do
     shared_examples_for 'projects response' do
       it 'returns an array of projects' do
-        get api('/projects', current_user)
+        get api('/projects', current_user), filter
 
         expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
         expect(json_response).to be_an Array
         expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
       end
@@ -61,6 +62,7 @@ describe API::Projects, api: true  do
 
     context 'when unauthenticated' do
       it_behaves_like 'projects response' do
+        let(:filter) { {} }
         let(:current_user) { nil }
         let(:projects) { [public_project] }
       end
@@ -68,6 +70,7 @@ describe API::Projects, api: true  do
 
     context 'when authenticated as regular user' do
       it_behaves_like 'projects response' do
+        let(:filter) { {} }
         let(:current_user) { user }
         let(:projects) { [public_project, project, project2, project3] }
       end
@@ -121,7 +124,7 @@ describe API::Projects, api: true  do
 
       context 'and with simple=true' do
         it 'returns a simplified version of all the projects' do
-          expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+          expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
 
           get api('/projects?simple=true', user)
 
@@ -133,13 +136,18 @@ describe API::Projects, api: true  do
       end
 
       context 'and using search' do
-        it 'returns searched project' do
-          get api('/projects', user), { search: project.name }
+        it_behaves_like 'projects response' do
+          let(:filter) { { search: project.name } }
+          let(:current_user) { user }
+          let(:projects) { [project] }
+        end
+      end
 
-          expect(response).to have_http_status(200)
-          expect(response).to include_pagination_headers
-          expect(json_response).to be_an Array
-          expect(json_response.length).to eq(1)
+      context 'and membership=true' do
+        it_behaves_like 'projects response' do
+          let(:filter) { { membership: true } }
+          let(:current_user) { user }
+          let(:projects) { [project, project2, project3] }
         end
       end
 
@@ -216,36 +224,52 @@ describe API::Projects, api: true  do
       end
 
       context 'and with all query parameters' do
-        # |         | project5 | project6 | project7 | project8 | project9 |
-        # |---------+----------+----------+----------+----------+----------|
-        # | search  | x        |          | x        | x        | x        |
-        # | starred | x        | x        |          | x        | x        |
-        # | public  | x        | x        | x        |          | x        |
-        # | owned   | x        | x        | x        | x        |          |
-        let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) }
+        let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: create(:namespace)) }
         let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) }
         let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) }
         let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) }
         let!(:project9) { create(:empty_project, :public, path: 'gitlab9') }
 
         before do
-          user.update_attributes(starred_projects: [project5, project6, project8, project9])
+          user.update_attributes(starred_projects: [project5, project7, project8, project9])
         end
 
-        it 'returns only projects that satify all query parameters' do
-          get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
+        context 'including owned filter' do
+          it 'returns only projects that satisfy all query parameters' do
+            get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
 
-          expect(response).to have_http_status(200)
-          expect(response).to include_pagination_headers
-          expect(json_response).to be_an Array
-          expect(json_response.size).to eq(1)
-          expect(json_response.first['id']).to eq(project5.id)
+            expect(response).to have_http_status(200)
+            expect(response).to include_pagination_headers
+            expect(json_response).to be_an Array
+            expect(json_response.size).to eq(1)
+            expect(json_response.first['id']).to eq(project7.id)
+          end
+        end
+
+        context 'including membership filter' do
+          before do
+            create(:project_member,
+                   user: user,
+                   project: project5,
+                   access_level: ProjectMember::MASTER)
+          end
+
+          it 'returns only projects that satisfy all query parameters' do
+            get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
+
+            expect(response).to have_http_status(200)
+            expect(response).to include_pagination_headers
+            expect(json_response).to be_an Array
+            expect(json_response.size).to eq(2)
+            expect(json_response.map { |project| project['id'] }).to contain_exactly(project5.id, project7.id)
+          end
         end
       end
     end
 
     context 'when authenticated as a different user' do
       it_behaves_like 'projects response' do
+        let(:filter) { {} }
         let(:current_user) { user2 }
         let(:projects) { [public_project] }
       end
@@ -253,6 +277,7 @@ describe API::Projects, api: true  do
 
     context 'when authenticated as admin' do
       it_behaves_like 'projects response' do
+        let(:filter) { {} }
         let(:current_user) { admin }
         let(:projects) { Project.all }
       end
@@ -269,10 +294,37 @@ describe API::Projects, api: true  do
       end
     end
 
-    it 'creates new project without path and return 201' do
-      expect { post api('/projects', user), name: 'foo' }.
+    it 'creates new project without path but with name and returns 201' do
+      expect { post api('/projects', user), name: 'Foo Project' }.
         to change { Project.count }.by(1)
       expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('Foo Project')
+      expect(project.path).to eq('foo-project')
+    end
+
+    it 'creates new project without name but with path and returns 201' do
+      expect { post api('/projects', user), path: 'foo_project' }.
+        to change { Project.count }.by(1)
+      expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('foo_project')
+      expect(project.path).to eq('foo_project')
+    end
+
+    it 'creates new project name and path and returns 201' do
+      expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+        to change { Project.count }.by(1)
+      expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('Foo Project')
+      expect(project.path).to eq('foo-Project')
     end
 
     it 'creates last project before reaching project limit' do
@@ -281,7 +333,7 @@ describe API::Projects, api: true  do
       expect(response).to have_http_status(201)
     end
 
-    it 'does not create new project without name and return 400' do
+    it 'does not create new project without name or path and returns 400' do
       expect { post api('/projects', user) }.not_to change { Project.count }
       expect(response).to have_http_status(400)
     end
@@ -293,7 +345,7 @@ describe API::Projects, api: true  do
         issues_enabled: false,
         merge_requests_enabled: false,
         wiki_enabled: false,
-        only_allow_merge_if_build_succeeds: false,
+        only_allow_merge_if_pipeline_succeeds: false,
         request_access_enabled: true,
         only_allow_merge_if_all_discussions_are_resolved: false
       })
@@ -313,36 +365,39 @@ describe API::Projects, api: true  do
     end
 
     it 'sets a project as public' do
-      project = attributes_for(:project, :public)
+      project = attributes_for(:project, visibility: 'public')
+
       post api('/projects', user), project
-      expect(json_response['public']).to be_truthy
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+
+      expect(json_response['visibility']).to eq('public')
     end
 
     it 'sets a project as internal' do
-      project = attributes_for(:project, :internal)
+      project = attributes_for(:project, visibility: 'internal')
+
       post api('/projects', user), project
-      expect(json_response['public']).to be_falsey
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+
+      expect(json_response['visibility']).to eq('internal')
     end
 
     it 'sets a project as private' do
-      project = attributes_for(:project, :private)
+      project = attributes_for(:project, visibility: 'private')
+
       post api('/projects', user), project
-      expect(json_response['public']).to be_falsey
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+
+      expect(json_response['visibility']).to eq('private')
     end
 
     it 'sets a project as allowing merge even if build fails' do
-      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+      project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
       post api('/projects', user), project
-      expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
     end
 
-    it 'sets a project as allowing merge only if build succeeds' do
-      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+    it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
+      project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
       post api('/projects', user), project
-      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
     end
 
     it 'sets a project as allowing merge even if discussions are unresolved' do
@@ -369,8 +424,16 @@ describe API::Projects, api: true  do
       expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
     end
 
+    it 'ignores import_url when it is nil' do
+      project = attributes_for(:project, { import_url: nil })
+
+      post api('/projects', user), project
+
+      expect(response).to have_http_status(201)
+    end
+
     context 'when a visibility level is restricted' do
-      let(:project_param) { attributes_for(:project, :public) }
+      let(:project_param) { attributes_for(:project, visibility: 'public') }
 
       before do
         stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -388,10 +451,7 @@ describe API::Projects, api: true  do
       it 'allows an admin to override restricted visibility settings' do
         post api('/projects', admin), project_param
 
-        expect(json_response['public']).to be_truthy
-        expect(json_response['visibility_level']).to(
-          eq(Gitlab::VisibilityLevel::PUBLIC)
-        )
+        expect(json_response['visibility']).to eq('public')
       end
     end
   end
@@ -432,40 +492,41 @@ describe API::Projects, api: true  do
     end
 
     it 'sets a project as public' do
-      project = attributes_for(:project, :public)
+      project = attributes_for(:project, visibility: 'public')
+
       post api("/projects/user/#{user.id}", admin), project
 
       expect(response).to have_http_status(201)
-      expect(json_response['public']).to be_truthy
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+      expect(json_response['visibility']).to eq('public')
     end
 
     it 'sets a project as internal' do
-      project = attributes_for(:project, :internal)
+      project = attributes_for(:project, visibility: 'internal')
+
       post api("/projects/user/#{user.id}", admin), project
 
       expect(response).to have_http_status(201)
-      expect(json_response['public']).to be_falsey
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+      expect(json_response['visibility']).to eq('internal')
     end
 
     it 'sets a project as private' do
-      project = attributes_for(:project, :private)
+      project = attributes_for(:project, visibility: 'private')
+
       post api("/projects/user/#{user.id}", admin), project
-      expect(json_response['public']).to be_falsey
-      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+
+      expect(json_response['visibility']).to eq('private')
     end
 
     it 'sets a project as allowing merge even if build fails' do
-      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+      project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
       post api("/projects/user/#{user.id}", admin), project
-      expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
     end
 
-    it 'sets a project as allowing merge only if build succeeds' do
-      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+    it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
+      project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
       post api("/projects/user/#{user.id}", admin), project
-      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
     end
 
     it 'sets a project as allowing merge even if discussions are unresolved' do
@@ -529,9 +590,8 @@ describe API::Projects, api: true  do
         expect(json_response['description']).to eq(project.description)
         expect(json_response['default_branch']).to eq(project.default_branch)
         expect(json_response['tag_list']).to be_an Array
-        expect(json_response['public']).to be_falsey
         expect(json_response['archived']).to be_falsey
-        expect(json_response['visibility_level']).to be_present
+        expect(json_response['visibility']).to be_present
         expect(json_response['ssh_url_to_repo']).to be_present
         expect(json_response['http_url_to_repo']).to be_present
         expect(json_response['web_url']).to be_present
@@ -542,7 +602,7 @@ describe API::Projects, api: true  do
         expect(json_response['issues_enabled']).to be_present
         expect(json_response['merge_requests_enabled']).to be_present
         expect(json_response['wiki_enabled']).to be_present
-        expect(json_response['builds_enabled']).to be_present
+        expect(json_response['jobs_enabled']).to be_present
         expect(json_response['snippets_enabled']).to be_present
         expect(json_response['container_registry_enabled']).to be_present
         expect(json_response['created_at']).to be_present
@@ -553,13 +613,13 @@ describe API::Projects, api: true  do
         expect(json_response['avatar_url']).to be_nil
         expect(json_response['star_count']).to be_present
         expect(json_response['forks_count']).to be_present
-        expect(json_response['public_builds']).to be_present
+        expect(json_response['public_jobs']).to be_present
         expect(json_response['shared_with_groups']).to be_an Array
         expect(json_response['shared_with_groups'].length).to eq(1)
         expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
         expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
         expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
-        expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+        expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
         expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
       end
 
@@ -785,8 +845,7 @@ describe API::Projects, api: true  do
   describe 'POST /projects/:id/snippets' do
     it 'creates a new project snippet' do
       post api("/projects/#{project.id}/snippets", user),
-        title: 'api test', file_name: 'sample.rb', code: 'test',
-        visibility_level: Gitlab::VisibilityLevel::PRIVATE
+        title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private'
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('api test')
     end
@@ -820,8 +879,9 @@ describe API::Projects, api: true  do
     it 'deletes existing project snippet' do
       expect do
         delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+
+        expect(response).to have_http_status(204)
       end.to change { Snippet.count }.by(-1)
-      expect(response).to have_http_status(200)
     end
 
     it 'returns 404 when deleting unknown snippet id' do
@@ -905,8 +965,10 @@ describe API::Projects, api: true  do
           project_fork_target.reload
           expect(project_fork_target.forked_from_project).not_to be_nil
           expect(project_fork_target.forked?).to be_truthy
+
           delete api("/projects/#{project_fork_target.id}/fork", admin)
-          expect(response).to have_http_status(200)
+
+          expect(response).to have_http_status(204)
           project_fork_target.reload
           expect(project_fork_target.forked_from_project).to be_nil
           expect(project_fork_target.forked?).not_to be_truthy
@@ -1035,7 +1097,7 @@ describe API::Projects, api: true  do
       end
 
       it 'updates visibility_level' do
-        project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+        project_param = { visibility: 'public' }
         put api("/projects/#{project3.id}", user), project_param
         expect(response).to have_http_status(200)
         project_param.each_pair do |k, v|
@@ -1045,13 +1107,13 @@ describe API::Projects, api: true  do
 
       it 'updates visibility_level from public to private' do
         project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
-        project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+        project_param = { visibility: 'private' }
         put api("/projects/#{project3.id}", user), project_param
         expect(response).to have_http_status(200)
         project_param.each_pair do |k, v|
           expect(json_response[k.to_s]).to eq(v)
         end
-        expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+        expect(json_response['visibility']).to eq('private')
       end
 
       it 'does not update name to existing name' do
@@ -1118,7 +1180,7 @@ describe API::Projects, api: true  do
       end
 
       it 'does not update visibility_level' do
-        project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+        project_param = { visibility: 'public' }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(403)
       end
@@ -1263,7 +1325,9 @@ describe API::Projects, api: true  do
     context 'when authenticated as user' do
       it 'removes project' do
         delete api("/projects/#{project.id}", user)
-        expect(response).to have_http_status(200)
+
+        expect(response).to have_http_status(202)
+        expect(json_response['message']).to eql('202 Accepted')
       end
 
       it 'does not remove a project if not an owner' do
@@ -1287,7 +1351,9 @@ describe API::Projects, api: true  do
     context 'when authenticated as admin' do
       it 'removes any existing project' do
         delete api("/projects/#{project.id}", admin)
-        expect(response).to have_http_status(200)
+
+        expect(response).to have_http_status(202)
+        expect(json_response['message']).to eql('202 Accepted')
       end
 
       it 'does not remove a non existing project' do
@@ -1422,4 +1488,53 @@ describe API::Projects, api: true  do
       end
     end
   end
+
+  describe 'POST /projects/:id/housekeeping' do
+    let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+    before do
+      allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping)
+    end
+
+    context 'when authenticated as owner' do
+      it 'starts the housekeeping process' do
+        expect(housekeeping).to receive(:execute).once
+
+        post api("/projects/#{project.id}/housekeeping", user)
+
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when housekeeping lease is taken' do
+        it 'returns conflict' do
+          expect(housekeeping).to receive(:execute).once.and_raise(Projects::HousekeepingService::LeaseTaken)
+
+          post api("/projects/#{project.id}/housekeeping", user)
+
+          expect(response).to have_http_status(409)
+          expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/)
+        end
+      end
+    end
+
+    context 'when authenticated as developer' do
+      before do
+        project_member2
+      end
+
+      it 'returns forbidden error' do
+        post api("/projects/#{project.id}/housekeeping", user3)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        post api("/projects/#{project.id}/housekeeping")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 7652606a4910cb514a5e2311202f23756cfaf732..4783d011d5452c0cee5e3db2d733ffe03be0d2d1 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -30,7 +30,7 @@ describe API::Repositories, api: true  do
 
       context 'when ref does not exist' do
         it_behaves_like '404 response' do
-          let(:request) { get api("#{route}?ref_name=foo", current_user) }
+          let(:request) { get api("#{route}?ref=foo", current_user) }
           let(:message) { '404 Tree Not Found' }
         end
       end
@@ -66,7 +66,7 @@ describe API::Repositories, api: true  do
 
         context 'when ref does not exist' do
           it_behaves_like '404 response' do
-            let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) }
+            let(:request) { get api("#{route}?recursive=1&ref=foo", current_user) }
             let(:message) { '404 Tree Not Found' }
           end
         end
@@ -100,82 +100,70 @@ describe API::Repositories, api: true  do
     end
   end
 
-  {
-    'blobs/:sha' => 'blobs/master',
-    'commits/:sha/blob' => 'commits/master/blob'
-  }.each do |desc_path, example_path|
-    describe "GET /projects/:id/repository/#{desc_path}" do
-      let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+  describe "GET /projects/:id/repository/blobs/:sha" do
+    let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" }
 
-      shared_examples_for 'repository blob' do
-        it 'returns the repository blob' do
-          get api(route, current_user)
-
-          expect(response).to have_http_status(200)
-        end
-
-        context 'when sha does not exist' do
-          it_behaves_like '404 response' do
-            let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) }
-            let(:message) { '404 Commit Not Found' }
-          end
-        end
+    shared_examples_for 'repository blob' do
+      it 'returns blob attributes as json' do
+        get api(route, current_user)
 
-        context 'when filepath does not exist' do
-          it_behaves_like '404 response' do
-            let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) }
-            let(:message) { '404 File Not Found' }
-          end
-        end
+        expect(response).to have_http_status(200)
+        expect(json_response['size']).to eq(111)
+        expect(json_response['encoding']).to eq("base64")
+        expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+        expect(json_response['sha']).to eq(sample_blob.oid)
+      end
 
-        context 'when no filepath is given' do
-          it_behaves_like '400 response' do
-            let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) }
-          end
+      context 'when sha does not exist' do
+        it_behaves_like '404 response' do
+          let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) }
+          let(:message) { '404 Blob Not Found' }
         end
+      end
 
-        context 'when repository is disabled' do
-          include_context 'disabled repository'
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
 
-          it_behaves_like '403 response' do
-            let(:request) { get api(route, current_user) }
-          end
+        it_behaves_like '403 response' do
+          let(:request) { get api(route, current_user) }
         end
       end
+    end
 
-      context 'when unauthenticated', 'and project is public' do
-        it_behaves_like 'repository blob' do
-          let(:project) { create(:project, :public, :repository) }
-          let(:current_user) { nil }
-        end
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository blob' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
       end
+    end
 
-      context 'when unauthenticated', 'and project is private' do
-        it_behaves_like '404 response' do
-          let(:request) { get api(route) }
-          let(:message) { '404 Project Not Found' }
-        end
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route) }
+        let(:message) { '404 Project Not Found' }
       end
+    end
 
-      context 'when authenticated', 'as a developer' do
-        it_behaves_like 'repository blob' do
-          let(:current_user) { user }
-        end
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository blob' do
+        let(:current_user) { user }
       end
+    end
 
-      context 'when authenticated', 'as a guest' do
-        it_behaves_like '403 response' do
-          let(:request) { get api(route, guest) }
-        end
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route, guest) }
       end
     end
   end
 
-  describe "GET /projects/:id/repository/raw_blobs/:sha" do
-    let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+  describe "GET /projects/:id/repository/blobs/:sha/raw" do
+    let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}/raw" }
 
     shared_examples_for 'repository raw blob' do
       it 'returns the repository raw blob' do
+        expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
         get api(route, current_user)
 
         expect(response).to have_http_status(200)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..044b989e5ba31b85b1faf8be4e3e5f2e7d872f6d
--- /dev/null
+++ b/spec/requests/api/runner_spec.rb
@@ -0,0 +1,1072 @@
+require 'spec_helper'
+
+describe API::Runner do
+  include ApiHelpers
+  include StubGitlabCalls
+
+  let(:registration_token) { 'abcdefg123456' }
+
+  before do
+    stub_gitlab_calls
+    stub_application_setting(runners_registration_token: registration_token)
+  end
+
+  describe '/api/v4/runners' do
+    describe 'POST /api/v4/runners' do
+      context 'when no token is provided' do
+        it 'returns 400 error' do
+          post api('/runners')
+
+          expect(response).to have_http_status 400
+        end
+      end
+
+      context 'when invalid token is provided' do
+        it 'returns 403 error' do
+          post api('/runners'), token: 'invalid'
+
+          expect(response).to have_http_status 403
+        end
+      end
+
+      context 'when valid token is provided' do
+        it 'creates runner with default values' do
+          post api('/runners'), token: registration_token
+
+          runner = Ci::Runner.first
+
+          expect(response).to have_http_status 201
+          expect(json_response['id']).to eq(runner.id)
+          expect(json_response['token']).to eq(runner.token)
+          expect(runner.run_untagged).to be true
+          expect(runner.token).not_to eq(registration_token)
+        end
+
+        context 'when project token is used' do
+          let(:project) { create(:empty_project) }
+
+          it 'creates runner' do
+            post api('/runners'), token: project.runners_token
+
+            expect(response).to have_http_status 201
+            expect(project.runners.size).to eq(1)
+            expect(Ci::Runner.first.token).not_to eq(registration_token)
+            expect(Ci::Runner.first.token).not_to eq(project.runners_token)
+          end
+        end
+      end
+
+      context 'when runner description is provided' do
+        it 'creates runner' do
+          post api('/runners'), token: registration_token,
+                                description: 'server.hostname'
+
+          expect(response).to have_http_status 201
+          expect(Ci::Runner.first.description).to eq('server.hostname')
+        end
+      end
+
+      context 'when runner tags are provided' do
+        it 'creates runner' do
+          post api('/runners'), token: registration_token,
+                                tag_list: 'tag1, tag2'
+
+          expect(response).to have_http_status 201
+          expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
+        end
+      end
+
+      context 'when option for running untagged jobs is provided' do
+        context 'when tags are provided' do
+          it 'creates runner' do
+            post api('/runners'), token: registration_token,
+                                  run_untagged: false,
+                                  tag_list: ['tag']
+
+            expect(response).to have_http_status 201
+            expect(Ci::Runner.first.run_untagged).to be false
+            expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
+          end
+        end
+
+        context 'when tags are not provided' do
+          it 'returns 404 error' do
+            post api('/runners'), token: registration_token,
+                                  run_untagged: false
+
+            expect(response).to have_http_status 404
+          end
+        end
+      end
+
+      context 'when option for locking Runner is provided' do
+        it 'creates runner' do
+          post api('/runners'), token: registration_token,
+                                locked: true
+
+          expect(response).to have_http_status 201
+          expect(Ci::Runner.first.locked).to be true
+        end
+      end
+
+      %w(name version revision platform architecture).each do |param|
+        context "when info parameter '#{param}' info is present" do
+          let(:value) { "#{param}_value" }
+
+          it "updates provided Runner's parameter" do
+            post api('/runners'), token: registration_token,
+                                  info: { param => value }
+
+            expect(response).to have_http_status 201
+            expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
+          end
+        end
+      end
+    end
+
+    describe 'DELETE /api/v4/runners' do
+      context 'when no token is provided' do
+        it 'returns 400 error' do
+          delete api('/runners')
+
+          expect(response).to have_http_status 400
+        end
+      end
+
+      context 'when invalid token is provided' do
+        it 'returns 403 error' do
+          delete api('/runners'), token: 'invalid'
+
+          expect(response).to have_http_status 403
+        end
+      end
+
+      context 'when valid token is provided' do
+        let(:runner) { create(:ci_runner) }
+
+        it 'deletes Runner' do
+          delete api('/runners'), token: runner.token
+
+          expect(response).to have_http_status 204
+          expect(Ci::Runner.count).to eq(0)
+        end
+      end
+    end
+
+    describe 'POST /api/v4/runners/verify' do
+      let(:runner) { create(:ci_runner) }
+
+      context 'when no token is provided' do
+        it 'returns 400 error' do
+          post api('/runners/verify')
+
+          expect(response).to have_http_status :bad_request
+        end
+      end
+
+      context 'when invalid token is provided' do
+        it 'returns 403 error' do
+          post api('/runners/verify'), token: 'invalid-token'
+
+          expect(response).to have_http_status 403
+        end
+      end
+
+      context 'when valid token is provided' do
+        it 'verifies Runner credentials' do
+          post api('/runners/verify'), token: runner.token
+
+          expect(response).to have_http_status 200
+        end
+      end
+    end
+  end
+
+  describe '/api/v4/jobs' do
+    let(:project) { create(:empty_project, shared_runners_enabled: false) }
+    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
+    let(:runner) { create(:ci_runner) }
+    let!(:job) do
+      create(:ci_build, :artifacts, :extended_options,
+             pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
+    end
+
+    before { project.runners << runner }
+
+    describe 'POST /api/v4/jobs/request' do
+      let!(:last_update) {}
+      let!(:new_update) { }
+      let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }
+
+      before { stub_container_registry_config(enabled: false) }
+
+      shared_examples 'no jobs available' do
+        before { request_job }
+
+        context 'when runner sends version in User-Agent' do
+          context 'for stable version' do
+            it 'gives 204 and set X-GitLab-Last-Update' do
+              expect(response).to have_http_status(204)
+              expect(response.header).to have_key('X-GitLab-Last-Update')
+            end
+          end
+
+          context 'when last_update is up-to-date' do
+            let(:last_update) { runner.ensure_runner_queue_value }
+
+            it 'gives 204 and set the same X-GitLab-Last-Update' do
+              expect(response).to have_http_status(204)
+              expect(response.header['X-GitLab-Last-Update']).to eq(last_update)
+            end
+          end
+
+          context 'when last_update is outdated' do
+            let(:last_update) { runner.ensure_runner_queue_value }
+            let(:new_update) { runner.tick_runner_queue }
+
+            it 'gives 204 and set a new X-GitLab-Last-Update' do
+              expect(response).to have_http_status(204)
+              expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
+            end
+          end
+
+          context 'when beta version is sent' do
+            let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
+
+            it { expect(response).to have_http_status(204) }
+          end
+
+          context 'when pre-9-0 version is sent' do
+            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
+
+            it { expect(response).to have_http_status(204) }
+          end
+
+          context 'when pre-9-0 beta version is sent' do
+            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
+
+            it { expect(response).to have_http_status(204) }
+          end
+        end
+      end
+
+      context 'when no token is provided' do
+        it 'returns 400 error' do
+          post api('/jobs/request')
+
+          expect(response).to have_http_status 400
+        end
+      end
+
+      context 'when invalid token is provided' do
+        it 'returns 403 error' do
+          post api('/jobs/request'), token: 'invalid'
+
+          expect(response).to have_http_status 403
+        end
+      end
+
+      context 'when valid token is provided' do
+        context 'when Runner is not active' do
+          let(:runner) { create(:ci_runner, :inactive) }
+
+          it 'returns 204 error' do
+            request_job
+
+            expect(response).to have_http_status 204
+          end
+        end
+
+        context 'when jobs are finished' do
+          before { job.success }
+
+          it_behaves_like 'no jobs available'
+        end
+
+        context 'when other projects have pending jobs' do
+          before do
+            job.success
+            create(:ci_build, :pending)
+          end
+
+          it_behaves_like 'no jobs available'
+        end
+
+        context 'when shared runner requests job for project without shared_runners_enabled' do
+          let(:runner) { create(:ci_runner, :shared) }
+
+          it_behaves_like 'no jobs available'
+        end
+
+        context 'when there is a pending job' do
+          let(:expected_job_info) do
+            { 'name' => job.name,
+              'stage' => job.stage,
+              'project_id' => job.project.id,
+              'project_name' => job.project.name }
+          end
+
+          let(:expected_git_info) do
+            { 'repo_url' => job.repo_url,
+              'ref' => job.ref,
+              'sha' => job.sha,
+              'before_sha' => job.before_sha,
+              'ref_type' => 'branch' }
+          end
+
+          let(:expected_steps) do
+            [{ 'name' => 'script',
+               'script' => %w(ls date),
+               'timeout' => job.timeout,
+               'when' => 'on_success',
+               'allow_failure' => false },
+             { 'name' => 'after_script',
+               'script' => %w(ls date),
+               'timeout' => job.timeout,
+               'when' => 'always',
+               'allow_failure' => true }]
+          end
+
+          let(:expected_variables) do
+            [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+             { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
+             { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
+          end
+
+          let(:expected_artifacts) do
+            [{ 'name' => 'artifacts_file',
+               'untracked' => false,
+               'paths' => %w(out/),
+               'when' => 'always',
+               'expire_in' => '7d' }]
+          end
+
+          let(:expected_cache) do
+            [{ 'key' => 'cache_key',
+               'untracked' => false,
+               'paths' => ['vendor/*'] }]
+          end
+
+          it 'picks a job' do
+            request_job info: { platform: :darwin }
+
+            expect(response).to have_http_status(201)
+            expect(response.headers).not_to have_key('X-GitLab-Last-Update')
+            expect(runner.reload.platform).to eq('darwin')
+            expect(json_response['id']).to eq(job.id)
+            expect(json_response['token']).to eq(job.token)
+            expect(json_response['job_info']).to eq(expected_job_info)
+            expect(json_response['git_info']).to eq(expected_git_info)
+            expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' })
+            expect(json_response['services']).to eq([{ 'name' => 'postgres' }])
+            expect(json_response['steps']).to eq(expected_steps)
+            expect(json_response['artifacts']).to eq(expected_artifacts)
+            expect(json_response['cache']).to eq(expected_cache)
+            expect(json_response['variables']).to include(*expected_variables)
+          end
+
+          context 'when job is made for tag' do
+            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+
+            it 'sets branch as ref_type' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['git_info']['ref_type']).to eq('tag')
+            end
+          end
+
+          context 'when job is made for branch' do
+            it 'sets tag as ref_type' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['git_info']['ref_type']).to eq('branch')
+            end
+          end
+
+          it 'updates runner info' do
+            expect { request_job }.to change { runner.reload.contacted_at }
+          end
+
+          %w(name version revision platform architecture).each do |param|
+            context "when info parameter '#{param}' is present" do
+              let(:value) { "#{param}_value" }
+
+              it "updates provided Runner's parameter" do
+                request_job info: { param => value }
+
+                expect(response).to have_http_status(201)
+                expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
+              end
+            end
+          end
+
+          context 'when concurrently updating a job' do
+            before do
+              expect_any_instance_of(Ci::Build).to receive(:run!).
+                  and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
+            end
+
+            it 'returns a conflict' do
+              request_job
+
+              expect(response).to have_http_status(409)
+              expect(response.headers).not_to have_key('X-GitLab-Last-Update')
+            end
+          end
+
+          context 'when project and pipeline have multiple jobs' do
+            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+
+            before do
+              job.success
+              job2.success
+            end
+
+            it 'returns dependent jobs' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['id']).to eq(test_job.id)
+              expect(json_response['dependencies'].count).to eq(2)
+              expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
+                                                               { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+            end
+          end
+
+          context 'when explicit dependencies are defined' do
+            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+            let!(:test_job) do
+              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+                                stage: 'deploy', stage_idx: 1,
+                                options: { dependencies: [job2.name] })
+            end
+
+            before do
+              job.success
+              job2.success
+            end
+
+            it 'returns dependent jobs' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['id']).to eq(test_job.id)
+              expect(json_response['dependencies'].count).to eq(1)
+              expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
+            end
+          end
+
+          context 'when job has no tags' do
+            before { job.update(tags: []) }
+
+            context 'when runner is allowed to pick untagged jobs' do
+              before { runner.update_column(:run_untagged, true) }
+
+              it 'picks job' do
+                request_job
+
+                expect(response).to have_http_status 201
+              end
+            end
+
+            context 'when runner is not allowed to pick untagged jobs' do
+              before { runner.update_column(:run_untagged, false) }
+
+              it_behaves_like 'no jobs available'
+            end
+          end
+
+          context 'when triggered job is available' do
+            let(:expected_variables) do
+              [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+               { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
+               { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
+               { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true },
+               { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false },
+               { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
+            end
+
+            before do
+              trigger = create(:ci_trigger, project: project)
+              create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
+              project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+            end
+
+            it 'returns variables for triggers' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['variables']).to include(*expected_variables)
+            end
+          end
+
+          describe 'registry credentials support' do
+            let(:registry_url) { 'registry.example.com:5005' }
+            let(:registry_credentials) do
+              { 'type' => 'registry',
+                'url' => registry_url,
+                'username' => 'gitlab-ci-token',
+                'password' => job.token }
+            end
+
+            context 'when registry is enabled' do
+              before { stub_container_registry_config(enabled: true, host_port: registry_url) }
+
+              it 'sends registry credentials key' do
+                request_job
+
+                expect(json_response).to have_key('credentials')
+                expect(json_response['credentials']).to include(registry_credentials)
+              end
+            end
+
+            context 'when registry is disabled' do
+              before { stub_container_registry_config(enabled: false, host_port: registry_url) }
+
+              it 'does not send registry credentials' do
+                request_job
+
+                expect(json_response).to have_key('credentials')
+                expect(json_response['credentials']).not_to include(registry_credentials)
+              end
+            end
+          end
+        end
+
+        def request_job(token = runner.token, **params)
+          new_params = params.merge(token: token, last_update: last_update)
+          post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
+        end
+      end
+    end
+
+    describe 'PUT /api/v4/jobs/:id' do
+      let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
+
+      before { job.run! }
+
+      context 'when status is given' do
+        it 'mark job as succeeded' do
+          update_job(state: 'success')
+
+          expect(job.reload.status).to eq 'success'
+        end
+
+        it 'mark job as failed' do
+          update_job(state: 'failed')
+
+          expect(job.reload.status).to eq 'failed'
+        end
+      end
+
+      context 'when tace is given' do
+        it 'updates a running build' do
+          update_job(trace: 'BUILD TRACE UPDATED')
+
+          expect(response).to have_http_status(200)
+          expect(job.reload.trace).to eq 'BUILD TRACE UPDATED'
+        end
+      end
+
+      context 'when no trace is given' do
+        it 'does not override trace information' do
+          update_job
+
+          expect(job.reload.trace).to eq 'BUILD TRACE'
+        end
+      end
+
+      context 'when job has been erased' do
+        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+        it 'responds with forbidden' do
+          update_job
+
+          expect(response).to have_http_status(403)
+        end
+      end
+
+      def update_job(token = job.token, **params)
+        new_params = params.merge(token: token)
+        put api("/jobs/#{job.id}"), new_params
+      end
+    end
+
+    describe 'PATCH /api/v4/jobs/:id/trace' do
+      let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) }
+      let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
+      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+      let(:update_interval) { 10.seconds.to_i }
+
+      before { initial_patch_the_trace }
+
+      context 'when request is valid' do
+        it 'gets correct response' do
+          expect(response.status).to eq 202
+          expect(job.reload.trace).to eq 'BUILD TRACE appended'
+          expect(response.header).to have_key 'Range'
+          expect(response.header).to have_key 'Job-Status'
+        end
+
+        context 'when job has been updated recently' do
+          it { expect{ patch_the_trace }.not_to change { job.updated_at }}
+
+          it "changes the job's trace" do
+            patch_the_trace
+
+            expect(job.reload.trace).to eq 'BUILD TRACE appended appended'
+          end
+
+          context 'when Runner makes a force-patch' do
+            it { expect{ force_patch_the_trace }.not_to change { job.updated_at }}
+
+            it "doesn't change the build.trace" do
+              force_patch_the_trace
+
+              expect(job.reload.trace).to eq 'BUILD TRACE appended'
+            end
+          end
+        end
+
+        context 'when job was not updated recently' do
+          let(:update_interval) { 15.minutes.to_i }
+
+          it { expect { patch_the_trace }.to change { job.updated_at } }
+
+          it 'changes the job.trace' do
+            patch_the_trace
+
+            expect(job.reload.trace).to eq 'BUILD TRACE appended appended'
+          end
+
+          context 'when Runner makes a force-patch' do
+            it { expect { force_patch_the_trace }.to change { job.updated_at } }
+
+            it "doesn't change the job.trace" do
+              force_patch_the_trace
+
+              expect(job.reload.trace).to eq 'BUILD TRACE appended'
+            end
+          end
+        end
+
+        context 'when project for the build has been deleted' do
+          let(:job) do
+            create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job|
+              job.project.update(pending_delete: true)
+            end
+          end
+
+          it 'responds with forbidden' do
+            expect(response.status).to eq(403)
+          end
+        end
+      end
+
+      context 'when Runner makes a force-patch' do
+        before do
+          force_patch_the_trace
+        end
+
+        it 'gets correct response' do
+          expect(response.status).to eq 202
+          expect(job.reload.trace).to eq 'BUILD TRACE appended'
+          expect(response.header).to have_key 'Range'
+          expect(response.header).to have_key 'Job-Status'
+        end
+      end
+
+      context 'when content-range start is too big' do
+        let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+
+        it 'gets 416 error response with range headers' do
+          expect(response.status).to eq 416
+          expect(response.header).to have_key 'Range'
+          expect(response.header['Range']).to eq '0-11'
+        end
+      end
+
+      context 'when content-range start is too small' do
+        let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+
+        it 'gets 416 error response with range headers' do
+          expect(response.status).to eq 416
+          expect(response.header).to have_key 'Range'
+          expect(response.header['Range']).to eq '0-11'
+        end
+      end
+
+      context 'when Content-Range header is missing' do
+        let(:headers_with_range) { headers }
+
+        it { expect(response.status).to eq 400 }
+      end
+
+      context 'when job has been errased' do
+        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+        it { expect(response.status).to eq 403 }
+      end
+
+      def patch_the_trace(content = ' appended', request_headers = nil)
+        unless request_headers
+          offset = job.trace_length
+          limit = offset + content.length - 1
+          request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
+        end
+
+        Timecop.travel(job.updated_at + update_interval) do
+          patch api("/jobs/#{job.id}/trace"), content, request_headers
+          job.reload
+        end
+      end
+
+      def initial_patch_the_trace
+        patch_the_trace(' appended', headers_with_range)
+      end
+
+      def force_patch_the_trace
+        2.times { patch_the_trace('') }
+      end
+    end
+
+    describe 'artifacts' do
+      let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
+      let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+      let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
+      let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
+      let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+      let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
+
+      before { job.run! }
+
+      describe 'POST /api/v4/jobs/:id/artifacts/authorize' do
+        context 'when using token as parameter' do
+          it 'authorizes posting artifacts to running job' do
+            authorize_artifacts_with_token_in_params
+
+            expect(response).to have_http_status(200)
+            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+            expect(json_response['TempPath']).not_to be_nil
+          end
+
+          it 'fails to post too large artifact' do
+            stub_application_setting(max_artifacts_size: 0)
+
+            authorize_artifacts_with_token_in_params(filesize: 100)
+
+            expect(response).to have_http_status(413)
+          end
+        end
+
+        context 'when using token as header' do
+          it 'authorizes posting artifacts to running job' do
+            authorize_artifacts_with_token_in_headers
+
+            expect(response).to have_http_status(200)
+            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+            expect(json_response['TempPath']).not_to be_nil
+          end
+
+          it 'fails to post too large artifact' do
+            stub_application_setting(max_artifacts_size: 0)
+
+            authorize_artifacts_with_token_in_headers(filesize: 100)
+
+            expect(response).to have_http_status(413)
+          end
+        end
+
+        context 'when using runners token' do
+          it 'fails to authorize artifacts posting' do
+            authorize_artifacts(token: job.project.runners_token)
+
+            expect(response).to have_http_status(403)
+          end
+        end
+
+        it 'reject requests that did not go through gitlab-workhorse' do
+          headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+          authorize_artifacts
+
+          expect(response).to have_http_status(500)
+        end
+
+        context 'authorization token is invalid' do
+          it 'responds with forbidden' do
+            authorize_artifacts(token: 'invalid', filesize: 100 )
+
+            expect(response).to have_http_status(403)
+          end
+        end
+
+        def authorize_artifacts(params = {}, request_headers = headers)
+          post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers
+        end
+
+        def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers)
+          params = params.merge(token: job.token)
+          authorize_artifacts(params, request_headers)
+        end
+
+        def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token)
+          authorize_artifacts(params, request_headers)
+        end
+      end
+
+      describe 'POST /api/v4/jobs/:id/artifacts' do
+        context 'when artifacts are being stored inside of tmp path' do
+          before do
+            # by configuring this path we allow to pass temp file from any path
+            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+          end
+
+          context 'when job has been erased' do
+            let(:job) { create(:ci_build, erased_at: Time.now) }
+
+            before do
+              upload_artifacts(file_upload, headers_with_token)
+            end
+
+            it 'responds with forbidden' do
+              upload_artifacts(file_upload, headers_with_token)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+
+          context 'when job is running' do
+            shared_examples 'successful artifacts upload' do
+              it 'updates successfully' do
+                expect(response).to have_http_status(201)
+              end
+            end
+
+            context 'when uses regular file post' do
+              before { upload_artifacts(file_upload, headers_with_token, false) }
+
+              it_behaves_like 'successful artifacts upload'
+            end
+
+            context 'when uses accelerated file post' do
+              before { upload_artifacts(file_upload, headers_with_token, true) }
+
+              it_behaves_like 'successful artifacts upload'
+            end
+
+            context 'when updates artifact' do
+              before do
+                upload_artifacts(file_upload2, headers_with_token)
+                upload_artifacts(file_upload, headers_with_token)
+              end
+
+              it_behaves_like 'successful artifacts upload'
+            end
+
+            context 'when using runners token' do
+              it 'responds with forbidden' do
+                upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
+
+                expect(response).to have_http_status(403)
+              end
+            end
+          end
+
+          context 'when artifacts file is too large' do
+            it 'fails to post too large artifact' do
+              stub_application_setting(max_artifacts_size: 0)
+
+              upload_artifacts(file_upload, headers_with_token)
+
+              expect(response).to have_http_status(413)
+            end
+          end
+
+          context 'when artifacts post request does not contain file' do
+            it 'fails to post artifacts without file' do
+              post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token
+
+              expect(response).to have_http_status(400)
+            end
+          end
+
+          context 'GitLab Workhorse is not configured' do
+            it 'fails to post artifacts without GitLab-Workhorse' do
+              post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {}
+
+              expect(response).to have_http_status(403)
+            end
+          end
+
+          context 'when setting an expire date' do
+            let(:default_artifacts_expire_in) {}
+            let(:post_data) do
+              { 'file.path' => file_upload.path,
+                'file.name' => file_upload.original_filename,
+                'expire_in' => expire_in }
+            end
+
+            before do
+              stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in)
+
+              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
+            end
+
+            context 'when an expire_in is given' do
+              let(:expire_in) { '7 days' }
+
+              it 'updates when specified' do
+                expect(response).to have_http_status(201)
+                expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
+              end
+            end
+
+            context 'when no expire_in is given' do
+              let(:expire_in) { nil }
+
+              it 'ignores if not specified' do
+                expect(response).to have_http_status(201)
+                expect(job.reload.artifacts_expire_at).to be_nil
+              end
+
+              context 'with application default' do
+                context 'when default is 5 days' do
+                  let(:default_artifacts_expire_in) { '5 days' }
+
+                  it 'sets to application default' do
+                    expect(response).to have_http_status(201)
+                    expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
+                  end
+                end
+
+                context 'when default is 0' do
+                  let(:default_artifacts_expire_in) { '0' }
+
+                  it 'does not set expire_in' do
+                    expect(response).to have_http_status(201)
+                    expect(job.reload.artifacts_expire_at).to be_nil
+                  end
+                end
+              end
+            end
+          end
+
+          context 'posts artifacts file and metadata file' do
+            let!(:artifacts) { file_upload }
+            let!(:metadata) { file_upload2 }
+
+            let(:stored_artifacts_file) { job.reload.artifacts_file.file }
+            let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
+            let(:stored_artifacts_size) { job.reload.artifacts_size }
+
+            before do
+              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
+            end
+
+            context 'when posts data accelerated by workhorse is correct' do
+              let(:post_data) do
+                { 'file.path' => artifacts.path,
+                  'file.name' => artifacts.original_filename,
+                  'metadata.path' => metadata.path,
+                  'metadata.name' => metadata.original_filename }
+              end
+
+              it 'stores artifacts and artifacts metadata' do
+                expect(response).to have_http_status(201)
+                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
+                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+                expect(stored_artifacts_size).to eq(71759)
+              end
+            end
+
+            context 'when there is no artifacts file in post data' do
+              let(:post_data) do
+                { 'metadata' => metadata }
+              end
+
+              it 'is expected to respond with bad request' do
+                expect(response).to have_http_status(400)
+              end
+
+              it 'does not store metadata' do
+                expect(stored_metadata_file).to be_nil
+              end
+            end
+          end
+        end
+
+        context 'when artifacts are being stored outside of tmp path' do
+          before do
+            # by configuring this path we allow to pass file from @tmpdir only
+            # but all temporary files are stored in system tmp directory
+            @tmpdir = Dir.mktmpdir
+            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+          end
+
+          after { FileUtils.remove_entry @tmpdir }
+
+          it' "fails to post artifacts for outside of tmp path"' do
+            upload_artifacts(file_upload, headers_with_token)
+
+            expect(response).to have_http_status(400)
+          end
+        end
+
+        def upload_artifacts(file, headers = {}, accelerated = true)
+          params = if accelerated
+                     { 'file.path' => file.path, 'file.name' => file.original_filename }
+                   else
+                     { 'file' => file }
+                   end
+          post api("/jobs/#{job.id}/artifacts"), params, headers
+        end
+      end
+
+      describe 'GET /api/v4/jobs/:id/artifacts' do
+        let(:token) { job.token }
+
+        before { download_artifact }
+
+        context 'when job has artifacts' do
+          let(:job) { create(:ci_build, :artifacts) }
+          let(:download_headers) do
+            { 'Content-Transfer-Encoding' => 'binary',
+              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+          end
+
+          context 'when using job token' do
+            it 'download artifacts' do
+              expect(response).to have_http_status(200)
+              expect(response.headers).to include download_headers
+            end
+          end
+
+          context 'when using runnners token' do
+            let(:token) { job.project.runners_token }
+
+            it 'responds with forbidden' do
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+
+        context 'when job does not has artifacts' do
+          it 'responds with not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        def download_artifact(params = {}, request_headers = headers)
+          params = params.merge(token: token)
+          get api("/jobs/#{job.id}/artifacts"), params, request_headers
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 103d6755888b15c874b7a3cab4c94dfedd87f3ec..8a82543a8308e7a11671180b746131a1b59f88ce 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -277,8 +277,9 @@ describe API::Runners, api: true  do
         it 'deletes runner' do
           expect do
             delete api("/runners/#{shared_runner.id}", admin)
+
+            expect(response).to have_http_status(204)
           end.to change{ Ci::Runner.shared.count }.by(-1)
-          expect(response).to have_http_status(200)
         end
       end
 
@@ -286,15 +287,17 @@ describe API::Runners, api: true  do
         it 'deletes unused runner' do
           expect do
             delete api("/runners/#{unused_specific_runner.id}", admin)
+
+            expect(response).to have_http_status(204)
           end.to change{ Ci::Runner.specific.count }.by(-1)
-          expect(response).to have_http_status(200)
         end
 
         it 'deletes used runner' do
           expect do
             delete api("/runners/#{specific_runner.id}", admin)
+
+            expect(response).to have_http_status(204)
           end.to change{ Ci::Runner.specific.count }.by(-1)
-          expect(response).to have_http_status(200)
         end
       end
 
@@ -327,8 +330,9 @@ describe API::Runners, api: true  do
         it 'deletes runner for one owned project' do
           expect do
             delete api("/runners/#{specific_runner.id}", user)
+
+            expect(response).to have_http_status(204)
           end.to change{ Ci::Runner.specific.count }.by(-1)
-          expect(response).to have_http_status(200)
         end
       end
     end
@@ -457,8 +461,9 @@ describe API::Runners, api: true  do
         it "disables project's runner" do
           expect do
             delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+            expect(response).to have_http_status(204)
           end.to change{ project.runners.count }.by(-1)
-          expect(response).to have_http_status(200)
         end
       end
 
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 776dc6556506bad9e8b4b94380bdb93e22b7388e..fd334934ca59a740186b32dd07021f66700aaf4c 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -55,7 +55,7 @@ describe API::Services, api: true  do
       it "deletes #{service}" do
         delete api("/projects/#{project.id}/services/#{dashed_service}", user)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
         project.send(service_method).reload
         expect(project.send(service_method).activated?).to be_falsey
       end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 794e2b5c04dd4546df619f13929fcd8d4c09bbb9..28fab2011a5fb29665170b93e0418301c3f0becc 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -87,5 +87,23 @@ describe API::Session, api: true  do
         expect(response).to have_http_status(400)
       end
     end
+
+    context "when user is blocked" do
+      it "returns authentication error" do
+        user.block
+        post api("/session"), email: user.username, password: user.password
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when user is ldap_blocked" do
+      it "returns authentication error" do
+        user.ldap_block
+        post api("/session"), email: user.username, password: user.password
+
+        expect(response).to have_http_status(401)
+      end
+    end
   end
 end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 91e3c333a02e63fa8402e582cb7d2c7d68086910..11b4b718e2c44e27db980e41e424fcf9401d645b 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -18,6 +18,9 @@ describe API::Settings, 'Settings', api: true  do
       expect(json_response['koding_url']).to be_nil
       expect(json_response['plantuml_enabled']).to be_falsey
       expect(json_response['plantuml_url']).to be_nil
+      expect(json_response['default_project_visibility']).to be_a String
+      expect(json_response['default_snippet_visibility']).to be_a String
+      expect(json_response['default_group_visibility']).to be_a String
     end
   end
 
@@ -30,8 +33,16 @@ describe API::Settings, 'Settings', api: true  do
 
       it "updates application settings" do
         put api("/application/settings", admin),
-          default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
-          plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
+          default_projects_limit: 3,
+          signin_enabled: false,
+          repository_storage: 'custom',
+          koding_enabled: true,
+          koding_url: 'http://koding.example.com',
+          plantuml_enabled: true,
+          plantuml_url: 'http://plantuml.example.com',
+          default_snippet_visibility: 'internal',
+          restricted_visibility_levels: ['public'],
+          default_artifacts_expire_in: '2 days'
         expect(response).to have_http_status(200)
         expect(json_response['default_projects_limit']).to eq(3)
         expect(json_response['signin_enabled']).to be_falsey
@@ -41,6 +52,9 @@ describe API::Settings, 'Settings', api: true  do
         expect(json_response['koding_url']).to eq('http://koding.example.com')
         expect(json_response['plantuml_enabled']).to be_truthy
         expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+        expect(json_response['default_snippet_visibility']).to eq('internal')
+        expect(json_response['restricted_visibility_levels']).to eq(['public'])
+        expect(json_response['default_artifacts_expire_in']).to eq('2 days')
       end
     end
 
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 41def7cd1d44c8189246d4ffda81649982378e54..5d75b47b3cd27f078077bf7caad0f88c45550906 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -74,7 +74,7 @@ describe API::Snippets, api: true do
     end
 
     it 'returns 404 for invalid snippet id' do
-      delete api("/snippets/1234", user)
+      get api("/snippets/1234/raw", user)
 
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Snippet Not Found')
@@ -87,7 +87,7 @@ describe API::Snippets, api: true do
         title: 'Test Title',
         file_name: 'test.rb',
         content: 'puts "hello world"',
-        visibility_level: Snippet::PUBLIC
+        visibility: 'public'
       }
     end
 
@@ -120,14 +120,14 @@ describe API::Snippets, api: true do
 
       context 'when the snippet is private' do
         it 'creates the snippet' do
-          expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+          expect { create_snippet(visibility: 'private') }.
             to change { Snippet.count }.by(1)
         end
       end
 
       context 'when the snippet is public' do
         it 'rejects the shippet' do
-          expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+          expect { create_snippet(visibility: 'public') }.
             not_to change { Snippet.count }
 
           expect(response).to have_http_status(400)
@@ -135,7 +135,7 @@ describe API::Snippets, api: true do
         end
 
         it 'creates a spam log' do
-          expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+          expect { create_snippet(visibility: 'public') }.
             to change { SpamLog.count }.by(1)
         end
       end
@@ -218,12 +218,12 @@ describe API::Snippets, api: true do
         let(:visibility_level) { Snippet::PRIVATE }
 
         it 'rejects the snippet' do
-          expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+          expect { update_snippet(title: 'Foo', visibility: 'public') }.
             not_to change { snippet.reload.title }
         end
 
         it 'creates a spam log' do
-          expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+          expect { update_snippet(title: 'Foo', visibility: 'public') }.
             to change { SpamLog.count }.by(1)
         end
       end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b59da632c0037e27f898b8ec132e1e471cb4b81a..d1e10f12657b576a5d5917b1d9c7a53c94cf784b 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -91,6 +91,8 @@ describe API::SystemHooks, api: true  do
     it "deletes a hook" do
       expect do
         delete api("/hooks/#{hook.id}", admin)
+
+        expect(response).to have_http_status(204)
       end.to change { SystemHook.count }.by(-1)
     end
 
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 8a4f078182f34ad295dc37ddb1956b983d9a362b..b132d033a618ae7bf06a99892138bb0ed70d30e2 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -137,8 +137,8 @@ describe API::Tags, api: true  do
       context 'delete tag' do
         it 'deletes an existing tag' do
           delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
-          expect(response).to have_http_status(200)
-          expect(json_response['tag_name']).to eq(tag_name)
+
+          expect(response).to have_http_status(204)
         end
 
         it 'raises 404 if the tag does not exist' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 8506e8fccdebe590ddf6abfe6601e5eab62b389c..2c83e119065cbddc7fde6d22e2a16a59a3d5c587 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -58,11 +58,11 @@ describe API::Templates, api: true  do
       expect(json_response['popular']).to be true
       expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
       expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
-      expect(json_response['description']).to include('A permissive license that is short and to the point.')
+      expect(json_response['description']).to include('A short and simple permissive license with conditions')
       expect(json_response['conditions']).to eq(%w[include-copyright])
       expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
       expect(json_response['limitations']).to eq(%w[no-liability])
-      expect(json_response['content']).to include('The MIT License (MIT)')
+      expect(json_response['content']).to include('MIT License')
     end
   end
 
@@ -73,7 +73,7 @@ describe API::Templates, api: true  do
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
       expect(json_response).to be_an Array
-      expect(json_response.size).to eq(15)
+      expect(json_response.size).to eq(12)
       expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
     end
 
@@ -102,7 +102,7 @@ describe API::Templates, api: true  do
         let(:license_type) { 'mit' }
 
         it 'returns the license text' do
-          expect(json_response['content']).to include('The MIT License (MIT)')
+          expect(json_response['content']).to include('MIT License')
         end
 
         it 'replaces placeholder values' do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index f35e963a14bf0f20ec6cfdaa811282a7ae20dc65..b789284fa8da155d88aef6d4a4a93e67f56a1083 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe API::Todos, api: true do
   include ApiHelpers
 
-  let(:project_1) { create(:empty_project) }
+  let(:project_1) { create(:empty_project, :test_repo) }
   let(:project_2) { create(:empty_project) }
   let(:author_1) { create(:user) }
   let(:author_2) { create(:user) }
@@ -11,7 +11,7 @@ describe API::Todos, api: true do
   let(:merge_request) { create(:merge_request, source_project: project_1) }
   let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
   let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
-  let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
+  let!(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) }
   let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
 
   before do
@@ -163,7 +163,7 @@ describe API::Todos, api: true do
 
   shared_examples 'an issuable' do |issuable_type|
     it 'creates a todo on an issuable' do
-      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
 
       expect(response.status).to eq(201)
       expect(json_response['project']).to be_a Hash
@@ -180,7 +180,7 @@ describe API::Todos, api: true do
     it 'returns 304 there already exist a todo on that issuable' do
       create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
 
-      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
 
       expect(response.status).to eq(304)
     end
@@ -195,7 +195,7 @@ describe API::Todos, api: true do
       guest = create(:user)
       project_1.team << [guest, :guest]
 
-      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
+      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
 
       if issuable_type == 'merge_requests'
         expect(response).to have_http_status(403)
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 92dfc2aa277308ec87d470e4f842cc0f47ac0350..d93a734f5b69d590cb0e3377fa7cec05cfbaf7f6 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -14,7 +14,7 @@ describe API::Triggers do
   let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
   let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
 
-  describe 'POST /projects/:project_id/trigger' do
+  describe 'POST /projects/:project_id/trigger/pipeline' do
     let!(:project2) { create(:project) }
     let(:options) do
       {
@@ -28,17 +28,20 @@ describe API::Triggers do
 
     context 'Handles errors' do
       it 'returns bad request if token is missing' do
-        post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+        post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master'
+
         expect(response).to have_http_status(400)
       end
 
       it 'returns not found if project is not found' do
-        post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+        post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master')
+
         expect(response).to have_http_status(404)
       end
 
       it 'returns unauthorized if token is for different project' do
-        post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+        post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master')
+
         expect(response).to have_http_status(401)
       end
     end
@@ -46,25 +49,21 @@ describe API::Triggers do
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'creates builds' do
-        post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+      it 'creates pipeline' do
+        post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
+
         expect(response).to have_http_status(201)
+        expect(json_response).to include('id' => pipeline.id)
         pipeline.builds.reload
         expect(pipeline.builds.pending.size).to eq(2)
         expect(pipeline.builds.size).to eq(5)
       end
 
-      it 'creates builds on webhook from other gitlab repository and branch' do
-        expect do
-          post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
-        end.to change(project.builds, :count).by(5)
-        expect(response).to have_http_status(201)
-      end
+      it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
+        post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
 
-      it 'returns bad request with no builds created if there\'s no commit for that ref' do
-        post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
         expect(response).to have_http_status(400)
-        expect(json_response['message']).to eq('No builds created')
+        expect(json_response['message']).to eq('No pipeline created')
       end
 
       context 'Validates variables' do
@@ -73,22 +72,46 @@ describe API::Triggers do
         end
 
         it 'validates variables to be a hash' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master')
+
           expect(response).to have_http_status(400)
           expect(json_response['error']).to eq('variables is invalid')
         end
 
         it 'validates variables needs to be a map of key-valued strings' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
         it 'creates trigger request with variables' do
-          post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+          post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master')
+
+          expect(response).to have_http_status(201)
+          expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables)
+        end
+      end
+    end
+
+    context 'when triggering a pipeline from a trigger token' do
+      it 'creates builds from the ref given in the URL, not in the body' do
+        expect do
+          post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+        end.to change(project.builds, :count).by(5)
+
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when ref contains a dot' do
+        it 'creates builds from the ref given in the URL, not in the body' do
+          project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+          expect do
+            post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+          end.to change(project.builds, :count).by(4)
+
           expect(response).to have_http_status(201)
-          pipeline.builds.reload
-          expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
         end
       end
     end
@@ -123,17 +146,17 @@ describe API::Triggers do
     end
   end
 
-  describe 'GET /projects/:id/triggers/:token' do
+  describe 'GET /projects/:id/triggers/:trigger_id' do
     context 'authenticated user with valid permissions' do
       it 'returns trigger details' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+        get api("/projects/#{project.id}/triggers/#{trigger.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response).to be_a(Hash)
       end
 
       it 'responds with 404 Not Found if requesting non-existing trigger' do
-        get api("/projects/#{project.id}/triggers/abcdef012345", user)
+        get api("/projects/#{project.id}/triggers/-5", user)
 
         expect(response).to have_http_status(404)
       end
@@ -141,7 +164,7 @@ describe API::Triggers do
 
     context 'authenticated user with invalid permissions' do
       it 'does not return triggers list' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+        get api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
 
         expect(response).to have_http_status(403)
       end
@@ -149,7 +172,7 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not return triggers list' do
-        get api("/projects/#{project.id}/triggers/#{trigger.token}")
+        get api("/projects/#{project.id}/triggers/#{trigger.id}")
 
         expect(response).to have_http_status(401)
       end
@@ -158,19 +181,31 @@ describe API::Triggers do
 
   describe 'POST /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'creates trigger' do
-        expect do
+      context 'with required parameters' do
+        it 'creates trigger' do
+          expect do
+            post api("/projects/#{project.id}/triggers", user),
+              description: 'trigger'
+          end.to change{project.triggers.count}.by(1)
+
+          expect(response).to have_http_status(201)
+          expect(json_response).to include('description' => 'trigger')
+        end
+      end
+
+      context 'without required parameters' do
+        it 'does not create trigger' do
           post api("/projects/#{project.id}/triggers", user)
-        end.to change{project.triggers.count}.by(1)
 
-        expect(response).to have_http_status(201)
-        expect(json_response).to be_a(Hash)
+          expect(response).to have_http_status(:bad_request)
+        end
       end
     end
 
     context 'authenticated user with invalid permissions' do
       it 'does not create trigger' do
-        post api("/projects/#{project.id}/triggers", user2)
+        post api("/projects/#{project.id}/triggers", user2),
+          description: 'trigger'
 
         expect(response).to have_http_status(403)
       end
@@ -178,24 +213,87 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not create trigger' do
-        post api("/projects/#{project.id}/triggers")
+        post api("/projects/#{project.id}/triggers"),
+          description: 'trigger'
 
         expect(response).to have_http_status(401)
       end
     end
   end
 
-  describe 'DELETE /projects/:id/triggers/:token' do
+  describe 'PUT /projects/:id/triggers/:trigger_id' do
+    context 'authenticated user with valid permissions' do
+      let(:new_description) { 'new description' }
+
+      it 'updates description' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
+          description: new_description
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to include('description' => new_description)
+        expect(trigger.reload.description).to eq(new_description)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update trigger' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update trigger' do
+        put api("/projects/#{project.id}/triggers/#{trigger.id}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do
+    context 'authenticated user with valid permissions' do
+      it 'updates owner' do
+        expect(trigger.owner).to be_nil
+
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to include('owner')
+        expect(trigger.reload.owner).to eq(user)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/triggers/:trigger_id' do
     context 'authenticated user with valid permissions' do
       it 'deletes trigger' do
         expect do
-          delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+          delete api("/projects/#{project.id}/triggers/#{trigger.id}", user)
+
+          expect(response).to have_http_status(204)
         end.to change{project.triggers.count}.by(-1)
-        expect(response).to have_http_status(200)
       end
 
       it 'responds with 404 Not Found if requesting non-existing trigger' do
-        delete api("/projects/#{project.id}/triggers/abcdef012345", user)
+        delete api("/projects/#{project.id}/triggers/-5", user)
 
         expect(response).to have_http_status(404)
       end
@@ -203,7 +301,7 @@ describe API::Triggers do
 
     context 'authenticated user with invalid permissions' do
       it 'does not delete trigger' do
-        delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+        delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
 
         expect(response).to have_http_status(403)
       end
@@ -211,7 +309,7 @@ describe API::Triggers do
 
     context 'unauthenticated user' do
       it 'does not delete trigger' do
-        delete api("/projects/#{project.id}/triggers/#{trigger.token}")
+        delete api("/projects/#{project.id}/triggers/#{trigger.id}")
 
         expect(response).to have_http_status(401)
       end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 603da9f49fca9fe1c4f81fb0ac73fb1baa7eb043..04e7837fd7ae3656333e8442866d1efcf05f34c7 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -10,6 +10,8 @@ describe API::Users, api: true  do
   let(:omniauth_user) { create(:omniauth_user) }
   let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
   let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+  let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 }
+  let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
 
   describe "GET /users" do
     context "when unauthenticated" do
@@ -540,10 +542,12 @@ describe API::Users, api: true  do
       it 'deletes existing key' do
         user.keys << key
         user.save
+
         expect do
           delete api("/users/#{user.id}/keys/#{key.id}", admin)
+
+          expect(response).to have_http_status(204)
         end.to change { user.keys.count }.by(-1)
-        expect(response).to have_http_status(200)
       end
 
       it 'returns 404 error if user not found' do
@@ -637,10 +641,12 @@ describe API::Users, api: true  do
       it 'deletes existing email' do
         user.emails << email
         user.save
+
         expect do
           delete api("/users/#{user.id}/emails/#{email.id}", admin)
+
+          expect(response).to have_http_status(204)
         end.to change { user.emails.count }.by(-1)
-        expect(response).to have_http_status(200)
       end
 
       it 'returns 404 error if user not found' do
@@ -671,10 +677,10 @@ describe API::Users, api: true  do
 
     it "deletes user" do
       delete api("/users/#{user.id}", admin)
-      expect(response).to have_http_status(200)
+
+      expect(response).to have_http_status(204)
       expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
       expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
-      expect(json_response['email']).to eq(user.email)
     end
 
     it "does not delete for unauthenticated user" do
@@ -724,7 +730,7 @@ describe API::Users, api: true  do
         get api("/user", user)
 
         expect(response).to have_http_status(200)
-        expect(response).to match_response_schema('user/public')
+        expect(response).to match_response_schema('public_api/v4/user/public')
         expect(json_response['id']).to eq(user.id)
       end
     end
@@ -743,7 +749,7 @@ describe API::Users, api: true  do
           get api("/user?private_token=#{admin_personal_access_token}")
 
           expect(response).to have_http_status(200)
-          expect(response).to match_response_schema('user/public')
+          expect(response).to match_response_schema('public_api/v4/user/public')
           expect(json_response['id']).to eq(admin.id)
         end
       end
@@ -753,7 +759,7 @@ describe API::Users, api: true  do
           get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
 
           expect(response).to have_http_status(200)
-          expect(response).to match_response_schema('user/login')
+          expect(response).to match_response_schema('public_api/v4/user/login')
           expect(json_response['id']).to eq(user.id)
         end
 
@@ -761,7 +767,7 @@ describe API::Users, api: true  do
           get api("/user?private_token=#{admin.private_token}")
 
           expect(response).to have_http_status(200)
-          expect(response).to match_response_schema('user/public')
+          expect(response).to match_response_schema('public_api/v4/user/public')
           expect(json_response['id']).to eq(admin.id)
         end
       end
@@ -869,10 +875,12 @@ describe API::Users, api: true  do
     it "deletes existed key" do
       user.keys << key
       user.save
+
       expect do
         delete api("/user/keys/#{key.id}", user)
+
+        expect(response).to have_http_status(204)
       end.to change{user.keys.count}.by(-1)
-      expect(response).to have_http_status(200)
     end
 
     it "returns 404 if key ID not found" do
@@ -976,10 +984,12 @@ describe API::Users, api: true  do
     it "deletes existed email" do
       user.emails << email
       user.save
+
       expect do
         delete api("/user/emails/#{email.id}", user)
+
+        expect(response).to have_http_status(204)
       end.to change{user.emails.count}.by(-1)
-      expect(response).to have_http_status(200)
     end
 
     it "returns 404 if email ID not found" do
@@ -1147,4 +1157,187 @@ describe API::Users, api: true  do
       expect(json_response['message']).to eq('404 User Not Found')
     end
   end
+
+  describe 'GET /users/:user_id/impersonation_tokens' do
+    let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+    let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+    let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+    let!(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) }
+
+    it 'returns a 404 error if user not found' do
+      get api("/users/#{not_existing_user_id}/impersonation_tokens", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 User Not Found')
+    end
+
+    it 'returns a 403 error when authenticated as normal user' do
+      get api("/users/#{not_existing_user_id}/impersonation_tokens", user)
+
+      expect(response).to have_http_status(403)
+      expect(json_response['message']).to eq('403 Forbidden')
+    end
+
+    it 'returns an array of all impersonated tokens' do
+      get api("/users/#{user.id}/impersonation_tokens", admin)
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(2)
+    end
+
+    it 'returns an array of active impersonation tokens if state active' do
+      get api("/users/#{user.id}/impersonation_tokens?state=active", admin)
+
+      expect(response).to have_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(1)
+      expect(json_response).to all(include('active' => true))
+    end
+
+    it 'returns an array of inactive personal access tokens if active is set to false' do
+      get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(1)
+      expect(json_response).to all(include('active' => false))
+    end
+  end
+
+  describe 'POST /users/:user_id/impersonation_tokens' do
+    let(:name) { 'my new pat' }
+    let(:expires_at) { '2016-12-28' }
+    let(:scopes) { %w(api read_user) }
+    let(:impersonation) { true }
+
+    it 'returns validation error if impersonation token misses some attributes' do
+      post api("/users/#{user.id}/impersonation_tokens", admin)
+
+      expect(response).to have_http_status(400)
+      expect(json_response['error']).to eq('name is missing')
+    end
+
+    it 'returns a 404 error if user not found' do
+      post api("/users/#{not_existing_user_id}/impersonation_tokens", admin),
+        name: name,
+        expires_at: expires_at
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 User Not Found')
+    end
+
+    it 'returns a 403 error when authenticated as normal user' do
+      post api("/users/#{user.id}/impersonation_tokens", user),
+        name: name,
+        expires_at: expires_at
+
+      expect(response).to have_http_status(403)
+      expect(json_response['message']).to eq('403 Forbidden')
+    end
+
+    it 'creates a impersonation token' do
+      post api("/users/#{user.id}/impersonation_tokens", admin),
+        name: name,
+        expires_at: expires_at,
+        scopes: scopes,
+        impersonation: impersonation
+
+      expect(response).to have_http_status(201)
+      expect(json_response['name']).to eq(name)
+      expect(json_response['scopes']).to eq(scopes)
+      expect(json_response['expires_at']).to eq(expires_at)
+      expect(json_response['id']).to be_present
+      expect(json_response['created_at']).to be_present
+      expect(json_response['active']).to be_falsey
+      expect(json_response['revoked']).to be_falsey
+      expect(json_response['token']).to be_present
+      expect(json_response['impersonation']).to eq(impersonation)
+    end
+  end
+
+  describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do
+    let!(:personal_access_token) { create(:personal_access_token, user: user) }
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+    it 'returns 404 error if user not found' do
+      get api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 User Not Found')
+    end
+
+    it 'returns a 404 error if impersonation token not found' do
+      get api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+    end
+
+    it 'returns a 404 error if token is not impersonation token' do
+      get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+    end
+
+    it 'returns a 403 error when authenticated as normal user' do
+      get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+
+      expect(response).to have_http_status(403)
+      expect(json_response['message']).to eq('403 Forbidden')
+    end
+
+    it 'returns a personal access token' do
+      get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['token']).to be_present
+      expect(json_response['impersonation']).to be_truthy
+    end
+  end
+
+  describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do
+    let!(:personal_access_token) { create(:personal_access_token, user: user) }
+    let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+    it 'returns a 404 error if user not found' do
+      delete api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 User Not Found')
+    end
+
+    it 'returns a 404 error if impersonation token not found' do
+      delete api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+    end
+
+    it 'returns a 404 error if token is not impersonation token' do
+      delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+    end
+
+    it 'returns a 403 error when authenticated as normal user' do
+      delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+
+      expect(response).to have_http_status(403)
+      expect(json_response['message']).to eq('403 Forbidden')
+    end
+
+    it 'revokes a impersonation token' do
+      delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+
+      expect(response).to have_http_status(204)
+      expect(impersonation_token.revoked).to be_falsey
+      expect(impersonation_token.reload.revoked).to be_truthy
+    end
+  end
 end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eeb4d128c1b72eb4a3c9ac0149294e24ad4eff41
--- /dev/null
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -0,0 +1,299 @@
+require 'spec_helper'
+
+describe API::V3::AwardEmoji, api: true  do
+  include ApiHelpers
+
+  let(:user)            { create(:user) }
+  let!(:project)        { create(:empty_project) }
+  let(:issue)           { create(:issue, project: project) }
+  let!(:award_emoji)    { create(:award_emoji, awardable: issue, user: user) }
+  let!(:merge_request)  { create(:merge_request, source_project: project, target_project: project) }
+  let!(:downvote)       { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+  let!(:note)           { create(:note, project: project, noteable: issue) }
+
+  before { project.team << [user, :master] }
+
+  describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
+    context 'on an issue' do
+      it "returns an array of award_emoji" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(award_emoji.name)
+      end
+
+      it "returns a 404 error when issue id not found" do
+        get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'on a merge request' do
+      it "returns an array of award_emoji" do
+        get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(downvote.name)
+      end
+    end
+
+    context 'on a snippet' do
+      let(:snippet) { create(:project_snippet, :public, project: project) }
+      let!(:award)  { create(:award_emoji, awardable: snippet) }
+
+      it 'returns the awarded emoji' do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(award.name)
+      end
+    end
+
+    context 'when the user has no access' do
+      it 'returns a status code 404' do
+        user1 = create(:user)
+
+        get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
+    let!(:rocket)  { create(:award_emoji, awardable: note, name: 'rocket') }
+
+    it 'returns an array of award emoji' do
+      get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.first['name']).to eq(rocket.name)
+    end
+  end
+
+  describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
+    context 'on an issue' do
+      it "returns the award emoji" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(award_emoji.name)
+        expect(json_response['awardable_id']).to eq(issue.id)
+        expect(json_response['awardable_type']).to eq("Issue")
+      end
+
+      it "returns a 404 error if the award is not found" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'on a merge request' do
+      it 'returns the award emoji' do
+        get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(downvote.name)
+        expect(json_response['awardable_id']).to eq(merge_request.id)
+        expect(json_response['awardable_type']).to eq("MergeRequest")
+      end
+    end
+
+    context 'on a snippet' do
+      let(:snippet) { create(:project_snippet, :public, project: project) }
+      let!(:award)  { create(:award_emoji, awardable: snippet) }
+
+      it 'returns the awarded emoji' do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(award.name)
+        expect(json_response['awardable_id']).to eq(snippet.id)
+        expect(json_response['awardable_type']).to eq("Snippet")
+      end
+    end
+
+    context 'when the user has no access' do
+      it 'returns a status code 404' do
+        user1 = create(:user)
+
+        get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
+    let!(:rocket)  { create(:award_emoji, awardable: note, name: 'rocket') }
+
+    it 'returns an award emoji' do
+      get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).not_to be_an Array
+      expect(json_response['name']).to eq(rocket.name)
+    end
+  end
+
+  describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+    let(:issue2)  { create(:issue, project: project, author: user) }
+
+    context "on an issue" do
+      it "creates a new award emoji" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+
+        expect(response).to have_http_status(201)
+        expect(json_response['name']).to eq('blowfish')
+        expect(json_response['user']['username']).to eq(user.username)
+      end
+
+      it "returns a 400 bad request error if the name is not given" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+        expect(response).to have_http_status(400)
+      end
+
+      it "returns a 401 unauthorized error if the user is not authenticated" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+
+        expect(response).to have_http_status(401)
+      end
+
+      it "returns a 404 error if the user authored issue" do
+        post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "normalizes +1 as thumbsup award" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+
+        expect(issue.award_emoji.last.name).to eq("thumbsup")
+      end
+
+      context 'when the emoji already has been awarded' do
+        it 'returns a 404 status code' do
+          post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+          post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+
+          expect(response).to have_http_status(404)
+          expect(json_response["message"]).to match("has already been taken")
+        end
+      end
+    end
+
+    context 'on a snippet' do
+      it 'creates a new award emoji' do
+        snippet = create(:project_snippet, :public, project: project)
+
+        post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
+
+        expect(response).to have_http_status(201)
+        expect(json_response['name']).to eq('blowfish')
+        expect(json_response['user']['username']).to eq(user.username)
+      end
+    end
+  end
+
+  describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+    let(:note2)  { create(:note, project: project, noteable: issue, author: user) }
+
+    it 'creates a new award emoji' do
+      expect do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+      end.to change { note.award_emoji.count }.from(0).to(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response['user']['username']).to eq(user.username)
+    end
+
+    it "it returns 404 error when user authored note" do
+      post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "normalizes +1 as thumbsup award" do
+      post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+
+      expect(note.award_emoji.last.name).to eq("thumbsup")
+    end
+
+    context 'when the emoji already has been awarded' do
+      it 'returns a 404 status code' do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+
+        expect(response).to have_http_status(404)
+        expect(json_response["message"]).to match("has already been taken")
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
+    context 'when the awardable is an Issue' do
+      it 'deletes the award' do
+        expect do
+          delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+          expect(response).to have_http_status(200)
+        end.to change { issue.award_emoji.count }.from(1).to(0)
+      end
+
+      it 'returns a 404 error when the award emoji can not be found' do
+        delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when the awardable is a Merge Request' do
+      it 'deletes the award' do
+        expect do
+          delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+          expect(response).to have_http_status(200)
+        end.to change { merge_request.award_emoji.count }.from(1).to(0)
+      end
+
+      it 'returns a 404 error when note id not found' do
+        delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when the awardable is a Snippet' do
+      let(:snippet) { create(:project_snippet, :public, project: project) }
+      let!(:award)  { create(:award_emoji, awardable: snippet, user: user) }
+
+      it 'deletes the award' do
+        expect do
+          delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+          expect(response).to have_http_status(200)
+        end.to change { snippet.award_emoji.count }.from(1).to(0)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+    let!(:rocket)  { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
+
+    it 'deletes the award' do
+      expect do
+        delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+        expect(response).to have_http_status(200)
+      end.to change { note.award_emoji.count }.from(1).to(0)
+    end
+  end
+end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index 8aaf3be4f876553ba9c65312ee5bd583dce25f3a..eb95934f354b6e4d2093ebe8354a619955049a7d 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -5,6 +5,7 @@ describe API::V3::Boards, api: true  do
 
   let(:user)        { create(:user) }
   let(:guest)       { create(:user) }
+  let(:non_member)  { create(:user) }
   let!(:project)    { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
 
   let!(:dev_label) do
@@ -76,4 +77,37 @@ describe API::V3::Boards, api: true  do
       expect(response).to have_http_status(404)
     end
   end
+
+  describe "DELETE /projects/:id/board/lists/:list_id" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it "rejects a non member from deleting a list" do
+      delete v3_api("#{base_url}/#{dev_list.id}", non_member)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it "rejects a user with guest role from deleting a list" do
+      delete v3_api("#{base_url}/#{dev_list.id}", guest)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it "returns 404 error if list id not found" do
+      delete v3_api("#{base_url}/44444", user)
+
+      expect(response).to have_http_status(404)
+    end
+
+    context "when the user is project owner" do
+      let(:owner)     { create(:user) }
+      let(:project)   { create(:empty_project, namespace: owner.namespace) }
+
+      it "deletes the list if an admin requests it" do
+        delete v3_api("#{base_url}/#{dev_list.id}", owner)
+
+        expect(response).to have_http_status(200)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index 0e4c6bc3bc63c6eebbabb49a221b84c2081978c5..5dcd4f21f4eda36a7ba16fcc8f7051bc3b26f3c4 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -5,8 +5,13 @@ describe API::V3::Branches, api: true  do
   include ApiHelpers
 
   let(:user) { create(:user) }
+  let(:user2) { create(:user) }
   let!(:project) { create(:project, :repository, creator: user) }
   let!(:master) { create(:project_member, :master, user: user, project: project) }
+  let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+  let!(:branch_name) { 'feature' }
+  let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+  let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
 
   describe "GET /projects/:id/repository/branches" do
     it "returns an array of project branches" do
@@ -20,4 +25,111 @@ describe API::V3::Branches, api: true  do
       expect(branch_names).to match_array(project.repository.branch_names)
     end
   end
+
+  describe "DELETE /projects/:id/repository/branches/:branch" do
+    before do
+      allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+    end
+
+    it "removes branch" do
+      delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['branch_name']).to eq(branch_name)
+    end
+
+    it "removes a branch with dots in the branch name" do
+      delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['branch_name']).to eq("with.1.2.3")
+    end
+
+    it 'returns 404 if branch not exists' do
+      delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it "removes protected branch" do
+      create(:protected_branch, project: project, name: branch_name)
+      delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+      expect(response).to have_http_status(405)
+      expect(json_response['message']).to eq('Protected branch cant be removed')
+    end
+
+    it "does not remove HEAD branch" do
+      delete v3_api("/projects/#{project.id}/repository/branches/master", user)
+      expect(response).to have_http_status(405)
+      expect(json_response['message']).to eq('Cannot remove HEAD branch')
+    end
+  end
+
+  describe "DELETE /projects/:id/repository/merged_branches" do
+    before do
+      allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+    end
+
+    it 'returns 200' do
+      delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
+
+      expect(response).to have_http_status(200)
+    end
+
+    it 'returns a 403 error if guest' do
+      delete v3_api("/projects/#{project.id}/repository/merged_branches", user2)
+
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  describe "POST /projects/:id/repository/branches" do
+    it "creates a new branch" do
+      post v3_api("/projects/#{project.id}/repository/branches", user),
+           branch_name: 'feature1',
+           ref: branch_sha
+
+      expect(response).to have_http_status(201)
+
+      expect(json_response['name']).to eq('feature1')
+      expect(json_response['commit']['id']).to eq(branch_sha)
+    end
+
+    it "denies for user without push access" do
+      post v3_api("/projects/#{project.id}/repository/branches", user2),
+           branch_name: branch_name,
+           ref: branch_sha
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns 400 if branch name is invalid' do
+      post v3_api("/projects/#{project.id}/repository/branches", user),
+           branch_name: 'new design',
+           ref: branch_sha
+      expect(response).to have_http_status(400)
+      expect(json_response['message']).to eq('Branch name is invalid')
+    end
+
+    it 'returns 400 if branch already exists' do
+      post v3_api("/projects/#{project.id}/repository/branches", user),
+           branch_name: 'new_design1',
+           ref: branch_sha
+      expect(response).to have_http_status(201)
+
+      post v3_api("/projects/#{project.id}/repository/branches", user),
+           branch_name: 'new_design1',
+           ref: branch_sha
+
+      expect(response).to have_http_status(400)
+      expect(json_response['message']).to eq('Branch already exists')
+    end
+
+    it 'returns 400 if ref name is invalid' do
+      post v3_api("/projects/#{project.id}/repository/branches", user),
+           branch_name: 'new_design3',
+           ref: 'foo'
+
+      expect(response).to have_http_status(400)
+      expect(json_response['message']).to eq('Invalid reference name')
+    end
+  end
 end
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06556401a2913c6e097656891415d041eb2af0e1
--- /dev/null
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe API::V3::BroadcastMessages, api: true do
+  include ApiHelpers
+
+  let(:user)  { create(:user) }
+  let(:admin) { create(:admin) }
+
+  describe 'DELETE /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      delete v3_api("/broadcast_messages/#{message.id}"),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      delete v3_api("/broadcast_messages/#{message.id}", user),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'deletes the broadcast message for admins' do
+      expect do
+        delete v3_api("/broadcast_messages/#{message.id}", admin)
+
+        expect(response).to have_http_status(200)
+      end.to change { BroadcastMessage.count }.by(-1)
+    end
+  end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
similarity index 88%
rename from spec/requests/api/builds_spec.rb
rename to spec/requests/api/v3/builds_spec.rb
index 38aef7f276767278f933c725b8ed26154f495657..a50c22a6dd1305ced3cb22c5551b821a204ff763 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe API::Builds, api: true do
+describe API::V3::Builds, api: true do
   include ApiHelpers
 
   let(:user) { create(:user) }
@@ -16,7 +16,9 @@ describe API::Builds, api: true do
     let(:query) { '' }
 
     before do
-      get api("/projects/#{project.id}/builds?#{query}", api_user)
+      create(:ci_build, :skipped, pipeline: pipeline)
+
+      get v3_api("/projects/#{project.id}/builds?#{query}", api_user)
     end
 
     context 'authorized user' do
@@ -49,6 +51,18 @@ describe API::Builds, api: true do
         end
       end
 
+      context 'filter project with scope skipped' do
+        let(:query) { 'scope=skipped' }
+        let(:json_build) { json_response.first }
+
+        it 'return builds with status skipped' do
+          expect(response).to have_http_status 200
+          expect(json_response).to be_an Array
+          expect(json_response.length).to eq 1
+          expect(json_build['status']).to eq 'skipped'
+        end
+      end
+
       context 'filter project with array of scope elements' do
         let(:query) { 'scope[0]=pending&scope[1]=running' }
 
@@ -77,7 +91,7 @@ describe API::Builds, api: true do
   describe 'GET /projects/:id/repository/commits/:sha/builds' do
     context 'when commit does not exist in repository' do
       before do
-        get api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
+        get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
       end
 
       it 'responds with 404' do
@@ -93,7 +107,7 @@ describe API::Builds, api: true do
             create(:ci_build, pipeline: pipeline)
             create(:ci_build)
 
-            get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
+            get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
           end
 
           it 'returns project jobs for specific commit' do
@@ -116,7 +130,7 @@ describe API::Builds, api: true do
         context 'when pipeline has no jobs' do
           before do
             branch_head = project.commit('feature').id
-            get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
+            get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
           end
 
           it 'returns an empty array' do
@@ -132,7 +146,7 @@ describe API::Builds, api: true do
           create(:ci_pipeline, project: project, sha: project.commit.id)
           create(:ci_build, pipeline: pipeline)
 
-          get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
+          get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
         end
 
         it 'does not return project jobs' do
@@ -145,7 +159,7 @@ describe API::Builds, api: true do
 
   describe 'GET /projects/:id/builds/:build_id' do
     before do
-      get api("/projects/#{project.id}/builds/#{build.id}", api_user)
+      get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user)
     end
 
     context 'authorized user' do
@@ -175,7 +189,7 @@ describe API::Builds, api: true do
 
   describe 'GET /projects/:id/builds/:build_id/artifacts' do
     before do
-      get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+      get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
     end
 
     context 'job with artifacts' do
@@ -217,7 +231,7 @@ describe API::Builds, api: true do
     end
 
     def path_for_ref(ref = pipeline.ref, job = build.name)
-      api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
+      v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
     end
 
     context 'when not logged in' do
@@ -310,7 +324,7 @@ describe API::Builds, api: true do
     let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
 
     before do
-      get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
+      get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
     end
 
     context 'authorized user' do
@@ -331,7 +345,7 @@ describe API::Builds, api: true do
 
   describe 'POST /projects/:id/builds/:build_id/cancel' do
     before do
-      post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
+      post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
     end
 
     context 'authorized user' do
@@ -364,7 +378,7 @@ describe API::Builds, api: true do
     let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
 
     before do
-      post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
+      post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
     end
 
     context 'authorized user' do
@@ -396,7 +410,7 @@ describe API::Builds, api: true do
 
   describe 'POST /projects/:id/builds/:build_id/erase' do
     before do
-      post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
+      post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user)
     end
 
     context 'job is erasable' do
@@ -426,7 +440,7 @@ describe API::Builds, api: true do
 
   describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
     before do
-      post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
+      post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
     end
 
     context 'artifacts did not expire' do
@@ -452,7 +466,7 @@ describe API::Builds, api: true do
 
   describe 'POST /projects/:id/builds/:build_id/play' do
     before do
-      post api("/projects/#{project.id}/builds/#{build.id}/play", user)
+      post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user)
     end
 
     context 'on an playable job' do
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 2d7584c3e5934fd06da951613a0eaf84286c6e37..adba3a787aab619fa7529197340dba4bfcf1bef6 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do
     end
   end
 
-  describe "Create a commit with multiple files and actions" do
+  describe "POST /projects/:id/repository/commits" do
     let!(:url) { "/projects/#{project.id}/repository/commits" }
 
     it 'returns a 403 unauthorized for user without permissions' do
@@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do
       expect(response).to have_http_status(400)
     end
 
-    context :create do
+    describe 'create' do
       let(:message) { 'Created file' }
       let!(:invalid_c_params) do
         {
@@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do
         expect(response).to have_http_status(400)
       end
 
-      context 'with project path in URL' do
-        let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+      context 'with project path containing a dot in URL' do
+        let!(:user) { create(:user, username: 'foo.bar') }
+        let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
 
         it 'a new file in project repo' do
           post v3_api(url, user), valid_c_params
@@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do
       end
     end
 
-    context :delete do
+    describe 'delete' do
       let(:message) { 'Deleted file' }
       let!(:invalid_d_params) do
         {
@@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do
       end
     end
 
-    context :move do
+    describe 'move' do
       let(:message) { 'Moved file' }
       let!(:invalid_m_params) do
         {
@@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do
       end
     end
 
-    context :update do
+    describe 'update' do
       let(:message) { 'Updated file' }
       let!(:invalid_u_params) do
         {
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c5ce407b32fe9ee51920415a47d1b9fb1f4df80
--- /dev/null
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe API::Deployments, api: true  do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:non_member)  { create(:user) }
+  let(:project)     { deployment.environment.project }
+  let!(:deployment) { create(:deployment) }
+
+  before do
+    project.team << [user, :master]
+  end
+
+  shared_examples 'a paginated resources' do
+    before do
+      # Fires the request
+      request
+    end
+
+    it 'has pagination headers' do
+      expect(response).to include_pagination_headers
+    end
+  end
+
+  describe 'GET /projects/:id/deployments' do
+    context 'as member of the project' do
+      it_behaves_like 'a paginated resources' do
+        let(:request) { get api("/projects/#{project.id}/deployments", user) }
+      end
+
+      it 'returns projects deployments' do
+        get api("/projects/#{project.id}/deployments", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(1)
+        expect(json_response.first['iid']).to eq(deployment.iid)
+        expect(json_response.first['sha']).to match /\A\h{40}\z/
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        get api("/projects/#{project.id}/deployments", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/deployments/:deployment_id' do
+    context 'as a member of the project' do
+      it 'returns the projects deployment' do
+        get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['sha']).to match /\A\h{40}\z/
+        expect(json_response['id']).to eq(deployment.id)
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..216192c9d3462693b0495fbc4d6a9fe7fa04fc7e
--- /dev/null
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -0,0 +1,165 @@
+require 'spec_helper'
+
+describe API::V3::Environments, api: true  do
+  include ApiHelpers
+
+  let(:user)          { create(:user) }
+  let(:non_member)    { create(:user) }
+  let(:project)       { create(:empty_project, :private, namespace: user.namespace) }
+  let!(:environment)  { create(:environment, project: project) }
+
+  before do
+    project.team << [user, :master]
+  end
+
+  shared_examples 'a paginated resources' do
+    before do
+      # Fires the request
+      request
+    end
+
+    it 'has pagination headers' do
+      expect(response.headers).to include('X-Total')
+      expect(response.headers).to include('X-Total-Pages')
+      expect(response.headers).to include('X-Per-Page')
+      expect(response.headers).to include('X-Page')
+      expect(response.headers).to include('X-Next-Page')
+      expect(response.headers).to include('X-Prev-Page')
+      expect(response.headers).to include('Link')
+    end
+  end
+
+  describe 'GET /projects/:id/environments' do
+    context 'as member of the project' do
+      it_behaves_like 'a paginated resources' do
+        let(:request) { get v3_api("/projects/#{project.id}/environments", user) }
+      end
+
+      it 'returns project environments' do
+        get v3_api("/projects/#{project.id}/environments", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(1)
+        expect(json_response.first['name']).to eq(environment.name)
+        expect(json_response.first['external_url']).to eq(environment.external_url)
+        expect(json_response.first['project']['id']).to eq(project.id)
+        expect(json_response.first['project']['visibility_level']).to be_present
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        get v3_api("/projects/#{project.id}/environments", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/environments' do
+    context 'as a member' do
+      it 'creates a environment with valid params' do
+        post v3_api("/projects/#{project.id}/environments", user), name: "mepmep"
+
+        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
+
+      it 'requires name to be passed' do
+        post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns a 400 if environment already exists' do
+        post v3_api("/projects/#{project.id}/environments", user), name: environment.name
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns a 400 if slug is specified' do
+        post v3_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
+      it 'rejects the request' do
+        post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a 400 when the required params are missing' do
+        post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/environments/:environment_id' do
+    it 'returns a 200 if name and external_url are changed' do
+      url = 'https://mepmep.whatever.ninja'
+      put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
+          name: 'Mepmep', external_url: url
+
+      expect(response).to have_http_status(200)
+      expect(json_response['name']).to eq('Mepmep')
+      expect(json_response['external_url']).to eq(url)
+    end
+
+    it "won't allow slug to be changed" do
+      slug = environment.slug
+      api_url = v3_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 v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
+          name: 'Mepmep'
+
+      expect(response).to have_http_status(200)
+      expect(json_response['name']).to eq('Mepmep')
+      expect(json_response['external_url']).to eq(url)
+    end
+
+    it 'returns a 404 if the environment does not exist' do
+      put v3_api("/projects/#{project.id}/environments/12345", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'DELETE /projects/:id/environments/:environment_id' do
+    context 'as a master' do
+      it 'returns a 200 for an existing environment' do
+        delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns a 404 for non existing id' do
+        delete v3_api("/projects/#{project.id}/environments/12345", user)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 Not found')
+      end
+    end
+
+    context 'a non member' do
+      it 'rejects the request' do
+        delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 4af05605ec66698b17e5fe4326f4a7f9367800c2..3b61139a2cd535a8fffcf01a9d229c638dc1ee48 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -2,17 +2,6 @@ require 'spec_helper'
 
 describe API::V3::Files, api: true  do
   include ApiHelpers
-  let(:user) { create(:user) }
-  let!(:project) { create(:project, :repository, namespace: user.namespace ) }
-  let(:guest) { create(:user) { |u| project.add_guest(u) } }
-  let(:file_path) { 'files/ruby/popen.rb' }
-  let(:params) do
-    {
-      file_path: file_path,
-      ref: 'master'
-    }
-  end
-  let(:author_email) { FFaker::Internet.email }
 
   # I have to remove periods from the end of the name
   # This happened when the user's name had a suffix (i.e. "Sr.")
@@ -26,6 +15,18 @@ describe API::V3::Files, api: true  do
   # ...
   # Author: Foo Sr <foo@example.com>
   # ...
+
+  let(:user) { create(:user) }
+  let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+  let(:guest) { create(:user) { |u| project.add_guest(u) } }
+  let(:file_path) { 'files/ruby/popen.rb' }
+  let(:params) do
+    {
+      file_path: file_path,
+      ref: 'master'
+    }
+  end
+  let(:author_email) { FFaker::Internet.email }
   let(:author_name) { FFaker::Name.name.chomp("\.") }
 
   before { project.team << [user, :developer] }
@@ -127,7 +128,7 @@ describe API::V3::Files, api: true  do
     end
 
     it "returns a 400 if editor fails to create file" do
-      allow_any_instance_of(Repository).to receive(:commit_file).
+      allow_any_instance_of(Repository).to receive(:create_file).
         and_return(false)
 
       post v3_api("/projects/#{project.id}/repository/files", user), valid_params
@@ -147,6 +148,20 @@ describe API::V3::Files, api: true  do
         expect(last_commit.author_name).to eq(author_name)
       end
     end
+
+    context 'when the repo is empty' do
+      let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+
+      it "creates a new file in project repo" do
+        post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['file_path']).to eq('newfile.rb')
+        last_commit = project.repository.commit.raw
+        expect(last_commit.author_email).to eq(user.email)
+        expect(last_commit.author_name).to eq(user.name)
+      end
+    end
   end
 
   describe "PUT /projects/:id/repository/files" do
@@ -215,7 +230,7 @@ describe API::V3::Files, api: true  do
     end
 
     it "returns a 400 if fails to create file" do
-      allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
+      allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
 
       delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
 
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a71b7d4b008801622393ab5a5b447a59d3d6d5d4
--- /dev/null
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -0,0 +1,565 @@
+require 'spec_helper'
+
+describe API::V3::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!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+  let!(:group2) { create(:group, :private) }
+  let!(:project1) { create(:empty_project, namespace: group1) }
+  let!(:project2) { create(:empty_project, namespace: group2) }
+  let!(:project3) { create(:empty_project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+
+  before do
+    group1.add_owner(user1)
+    group2.add_owner(user2)
+  end
+
+  describe "GET /groups" do
+    context "when unauthenticated" do
+      it "returns authentication error" do
+        get v3_api("/groups")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when authenticated as user" do
+      it "normal user: returns an array of groups of user1" do
+        get v3_api("/groups", user1)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(1)
+        expect(json_response)
+          .to satisfy_one { |group| group['name'] == group1.name }
+      end
+
+      it "does not include statistics" do
+        get v3_api("/groups", user1), statistics: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first).not_to include 'statistics'
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "admin: returns an array of all groups" do
+        get v3_api("/groups", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(2)
+      end
+
+      it "does not include statistics by default" do
+        get v3_api("/groups", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first).not_to include('statistics')
+      end
+
+      it "includes statistics if requested" do
+        attributes = {
+          storage_size: 702,
+          repository_size: 123,
+          lfs_objects_size: 234,
+          build_artifacts_size: 345,
+        }.stringify_keys
+
+        project1.statistics.update!(attributes)
+
+        get v3_api("/groups", admin), statistics: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response)
+          .to satisfy_one { |group| group['statistics'] == attributes }
+      end
+    end
+
+    context "when using skip_groups in request" do
+      it "returns all groups excluding skipped groups" do
+        get v3_api("/groups", admin), skip_groups: [group2.id]
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(1)
+      end
+    end
+
+    context "when using all_available in request" do
+      let(:response_groups) { json_response.map { |group| group['name'] } }
+
+      it "returns all groups you have access to" do
+        public_group = create :group, :public
+
+        get v3_api("/groups", user1), all_available: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_groups).to contain_exactly(public_group.name, group1.name)
+      end
+    end
+
+    context "when using sorting" do
+      let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
+      let(:response_groups) { json_response.map { |group| group['name'] } }
+
+      before do
+        group3.add_owner(user1)
+      end
+
+      it "sorts by name ascending by default" do
+        get v3_api("/groups", user1)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_groups).to eq([group3.name, group1.name])
+      end
+
+      it "sorts in descending order when passed" do
+        get v3_api("/groups", user1), sort: "desc"
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_groups).to eq([group1.name, group3.name])
+      end
+
+      it "sorts by the order_by param" do
+        get v3_api("/groups", user1), order_by: "path"
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_groups).to eq([group1.name, group3.name])
+      end
+    end
+  end
+
+  describe 'GET /groups/owned' do
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        get v3_api('/groups/owned')
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when authenticated as group owner' do
+      it 'returns an array of groups the user owns' do
+        get v3_api('/groups/owned', user2)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(group2.name)
+      end
+    end
+  end
+
+  describe "GET /groups/:id" do
+    context "when authenticated as user" do
+      it "returns one of user1's groups" do
+        project = create(:empty_project, namespace: group2, path: 'Foo')
+        create(:project_group_link, project: project, group: group1)
+
+        get v3_api("/groups/#{group1.id}", user1)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['id']).to eq(group1.id)
+        expect(json_response['name']).to eq(group1.name)
+        expect(json_response['path']).to eq(group1.path)
+        expect(json_response['description']).to eq(group1.description)
+        expect(json_response['visibility_level']).to eq(group1.visibility_level)
+        expect(json_response['avatar_url']).to eq(group1.avatar_url)
+        expect(json_response['web_url']).to eq(group1.web_url)
+        expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
+        expect(json_response['full_name']).to eq(group1.full_name)
+        expect(json_response['full_path']).to eq(group1.full_path)
+        expect(json_response['parent_id']).to eq(group1.parent_id)
+        expect(json_response['projects']).to be_an Array
+        expect(json_response['projects'].length).to eq(2)
+        expect(json_response['shared_projects']).to be_an Array
+        expect(json_response['shared_projects'].length).to eq(1)
+        expect(json_response['shared_projects'][0]['id']).to eq(project.id)
+      end
+
+      it "does not return a non existing group" do
+        get v3_api("/groups/1328", user1)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "does not return a group not attached to user1" do
+        get v3_api("/groups/#{group2.id}", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "returns any existing group" do
+        get v3_api("/groups/#{group2.id}", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(group2.name)
+      end
+
+      it "does not return a non existing group" do
+        get v3_api("/groups/1328", admin)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when using group path in URL' do
+      it 'returns any existing group' do
+        get v3_api("/groups/#{group1.path}", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(group1.name)
+      end
+
+      it 'does not return a non existing group' do
+        get v3_api('/groups/unknown', admin)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'does not return a group not attached to user1' do
+        get v3_api("/groups/#{group2.path}", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'PUT /groups/:id' do
+    let(:new_group_name) { 'New Group'}
+
+    context 'when authenticated as the group owner' do
+      it 'updates the group' do
+        put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(new_group_name)
+        expect(json_response['request_access_enabled']).to eq(true)
+      end
+
+      it 'returns 404 for a non existing group' do
+        put v3_api('/groups/1328', user1), name: new_group_name
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when authenticated as the admin' do
+      it 'updates the group' do
+        put v3_api("/groups/#{group1.id}", admin), name: new_group_name
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(new_group_name)
+      end
+    end
+
+    context 'when authenticated as an user that can see the group' do
+      it 'does not updates the group' do
+        put v3_api("/groups/#{group1.id}", user2), name: new_group_name
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'when authenticated as an user that cannot see the group' do
+      it 'returns 404 when trying to update the group' do
+        put v3_api("/groups/#{group2.id}", user1), name: new_group_name
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "GET /groups/:id/projects" do
+    context "when authenticated as user" do
+      it "returns the group's projects" do
+        get v3_api("/groups/#{group1.id}/projects", user1)
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(2)
+        project_names = json_response.map { |proj| proj['name'] }
+        expect(project_names).to match_array([project1.name, project3.name])
+        expect(json_response.first['visibility_level']).to be_present
+      end
+
+      it "returns the group's projects with simple representation" do
+        get v3_api("/groups/#{group1.id}/projects", user1), simple: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(2)
+        project_names = json_response.map { |proj| proj['name'] }
+        expect(project_names).to match_array([project1.name, project3.name])
+        expect(json_response.first['visibility_level']).not_to be_present
+      end
+
+      it 'filters the groups projects' do
+        public_project = create(:empty_project, :public, path: 'test1', group: group1)
+
+        get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an(Array)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(public_project.name)
+      end
+
+      it "does not return a non existing group" do
+        get v3_api("/groups/1328/projects", user1)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "does not return a group not attached to user1" do
+        get v3_api("/groups/#{group2.id}/projects", user1)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "only returns projects to which user has access" do
+        project3.team << [user3, :developer]
+
+        get v3_api("/groups/#{group1.id}/projects", user3)
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(project3.name)
+      end
+
+      it 'only returns the projects owned by user' do
+        project2.group.add_owner(user3)
+
+        get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(project2.name)
+      end
+
+      it 'only returns the projects starred by user' do
+        user1.starred_projects = [project1]
+
+        get v3_api("/groups/#{group1.id}/projects", user1), starred: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(project1.name)
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "returns any existing group" do
+        get v3_api("/groups/#{group2.id}/projects", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['name']).to eq(project2.name)
+      end
+
+      it "does not return a non existing group" do
+        get v3_api("/groups/1328/projects", admin)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when using group path in URL' do
+      it 'returns any existing group' do
+        get v3_api("/groups/#{group1.path}/projects", admin)
+
+        expect(response).to have_http_status(200)
+        project_names = json_response.map { |proj| proj['name'] }
+        expect(project_names).to match_array([project1.name, project3.name])
+      end
+
+      it 'does not return a non existing group' do
+        get v3_api('/groups/unknown/projects', admin)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'does not return a group not attached to user1' do
+        get v3_api("/groups/#{group2.path}/projects", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "POST /groups" do
+    context "when authenticated as user without group permissions" do
+      it "does not create group" do
+        post v3_api("/groups", user1), attributes_for(:group)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context "when authenticated as user with group permissions" do
+      it "creates group" do
+        group = attributes_for(:group, { request_access_enabled: false })
+
+        post v3_api("/groups", user3), group
+
+        expect(response).to have_http_status(201)
+
+        expect(json_response["name"]).to eq(group[:name])
+        expect(json_response["path"]).to eq(group[:path])
+        expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
+      end
+
+      it "creates a nested group" do
+        parent = create(:group)
+        parent.add_owner(user3)
+        group = attributes_for(:group, { parent_id: parent.id })
+
+        post v3_api("/groups", user3), group
+
+        expect(response).to have_http_status(201)
+
+        expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}")
+        expect(json_response["parent_id"]).to eq(parent.id)
+      end
+
+      it "does not create group, duplicate" do
+        post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
+
+        expect(response).to have_http_status(400)
+        expect(response.message).to eq("Bad Request")
+      end
+
+      it "returns 400 bad request error if name not given" do
+        post v3_api("/groups", user3), { path: group2.path }
+
+        expect(response).to have_http_status(400)
+      end
+
+      it "returns 400 bad request error if path not given" do
+        post v3_api("/groups", user3), { name: 'test' }
+
+        expect(response).to have_http_status(400)
+      end
+    end
+  end
+
+  describe "DELETE /groups/:id" do
+    context "when authenticated as user" do
+      it "removes group" do
+        delete v3_api("/groups/#{group1.id}", user1)
+
+        expect(response).to have_http_status(200)
+      end
+
+      it "does not remove a group if not an owner" do
+        user4 = create(:user)
+        group1.add_master(user4)
+
+        delete v3_api("/groups/#{group1.id}", user3)
+
+        expect(response).to have_http_status(403)
+      end
+
+      it "does not remove a non existing group" do
+        delete v3_api("/groups/1328", user1)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "does not remove a group not attached to user1" do
+        delete v3_api("/groups/#{group2.id}", user1)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "removes any existing group" do
+        delete v3_api("/groups/#{group2.id}", admin)
+
+        expect(response).to have_http_status(200)
+      end
+
+      it "does not remove a non existing group" do
+        delete v3_api("/groups/1328", admin)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "POST /groups/:id/projects/:project_id" do
+    let(:project) { create(:empty_project) }
+    let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+
+    before(:each) do
+      allow_any_instance_of(Projects::TransferService).
+        to receive(:execute).and_return(true)
+    end
+
+    context "when authenticated as user" do
+      it "does not transfer project to group" do
+        post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "transfers project to group" do
+        post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin)
+
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when using project path in URL' do
+        context 'with a valid project path' do
+          it "transfers project to group" do
+            post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin)
+
+            expect(response).to have_http_status(201)
+          end
+        end
+
+        context 'with a non-existent project path' do
+          it "does not transfer project to group" do
+            post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
+
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when using a group path in URL' do
+        context 'with a valid group path' do
+          it "transfers project to group" do
+            post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin)
+
+            expect(response).to have_http_status(201)
+          end
+        end
+
+        context 'with a non-existent group path' do
+          it "does not transfer project to group" do
+            post v3_api("/groups/noexist/projects/#{project_path}", admin)
+
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 8e6732fe23e2d2a955466bebe285ffa91ce72d1c..51021eec63cb81f9a3eff6c12389d8055c318bfe 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -232,6 +232,13 @@ describe API::V3::Issues, api: true  do
         expect(json_response).to be_an Array
         expect(response_dates).to eq(response_dates.sort)
       end
+
+      it 'matches V3 response schema' do
+        get v3_api('/issues', user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to match_response_schema('public_api/v3/issues')
+      end
     end
   end
 
@@ -432,6 +439,12 @@ describe API::V3::Issues, api: true  do
   describe "GET /projects/:id/issues" do
     let(:base_url) { "/projects/#{project.id}" }
 
+    it 'returns 404 when project does not exist' do
+      get v3_api('/projects/1000/issues', non_member)
+
+      expect(response).to have_http_status(404)
+    end
+
     it "returns 404 on private projects for other users" do
       private_project = create(:empty_project, :private)
       create(:issue, project: private_project)
@@ -722,7 +735,7 @@ describe API::V3::Issues, api: true  do
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new issue')
       expect(json_response['description']).to be_nil
-      expect(json_response['labels']).to eq(['label', 'label2'])
+      expect(json_response['labels']).to eq(%w(label label2))
       expect(json_response['confidential']).to be_falsy
     end
 
@@ -1281,6 +1294,6 @@ describe API::V3::Issues, api: true  do
   describe 'time tracking endpoints' do
     let(:issuable) { issue }
 
-    include_examples 'time tracking endpoints', 'issue'
+    include_examples 'V3 time tracking endpoints', 'issue'
   end
 end
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index bcb0c6b94492c8ba52206a7f8f42f699b2b23a79..dfac357d37c9daa0238e55c976273d599b898c48 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -21,11 +21,11 @@ describe API::V3::Labels, api: true  do
       create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
       create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
 
-      expected_keys = [
-        'id', 'name', 'color', 'description',
-        'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
-        'subscribed', 'priority'
-      ]
+      expected_keys = %w(
+        id name color description
+        open_issues_count closed_issues_count open_merge_requests_count
+        subscribed priority
+      )
 
       get v3_api("/projects/#{project.id}/labels", user)
 
@@ -149,4 +149,23 @@ describe API::V3::Labels, api: true  do
       end
     end
   end
+
+  describe 'DELETE /projects/:id/labels' do
+    it 'returns 200 for existing label' do
+      delete v3_api("/projects/#{project.id}/labels", user), name: 'label1'
+
+      expect(response).to have_http_status(200)
+    end
+
+    it 'returns 404 for non existing label' do
+      delete v3_api("/projects/#{project.id}/labels", user), name: 'label2'
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Label Not Found')
+    end
+
+    it 'returns 400 for wrong parameters' do
+      delete v3_api("/projects/#{project.id}/labels", user)
+      expect(response).to have_http_status(400)
+    end
+  end
 end
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index 28c3ca039601298b3a8da79eedc06852fdc92945..13814ed10c397d955804212470a89838b5b0f708 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe API::Members, api: true  do
+describe API::V3::Members, api: true  do
   include ApiHelpers
 
   let(:master) { create(:user) }
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c53800eef30cb58d8f0fdaec3c297fdcf88d7b97
--- /dev/null
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -0,0 +1,50 @@
+require "spec_helper"
+
+describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true  do
+  include ApiHelpers
+
+  let!(:user)          { create(:user) }
+  let!(:merge_request) { create(:merge_request, importing: true) }
+  let!(:project)       { merge_request.target_project }
+
+  before do
+    merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+    merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+    project.team << [user, :master]
+  end
+
+  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+    it 'returns 200 for a valid merge request' do
+      get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+      merge_request_diff = merge_request.merge_request_diffs.first
+
+      expect(response.status).to eq 200
+      expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
+      expect(json_response.first['id']).to eq(merge_request_diff.id)
+      expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+    end
+
+    it 'returns a 404 when merge_request_id not found' do
+      get v3_api("/projects/#{project.id}/merge_requests/999/versions", user)
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+    it 'returns a 200 for a valid merge request' do
+      merge_request_diff = merge_request.merge_request_diffs.first
+      get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+
+      expect(response.status).to eq 200
+      expect(json_response['id']).to eq(merge_request_diff.id)
+      expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+      expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
+    end
+
+    it 'returns a 404 when merge_request_id not found' do
+      get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index b94e1ef4cedf383a7254681a950a9aabf085d8ef..d73e9635c9b91043676778466fac7decb8a4945a 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -73,6 +73,13 @@ describe API::MergeRequests, api: true  do
         expect(json_response.first['title']).to eq(merge_request_merged.title)
       end
 
+      it 'matches V3 response schema' do
+        get v3_api("/projects/#{project.id}/merge_requests", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to match_response_schema('public_api/v3/merge_requests')
+      end
+
       context "with ordering" do
         before do
           @mr_later = mr_with_later_created_and_updated_at_time
@@ -237,7 +244,7 @@ describe API::MergeRequests, api: true  do
 
         expect(response).to have_http_status(201)
         expect(json_response['title']).to eq('Test merge_request')
-        expect(json_response['labels']).to eq(['label', 'label2'])
+        expect(json_response['labels']).to eq(%w(label label2))
         expect(json_response['milestone']['id']).to eq(milestone.id)
         expect(json_response['force_remove_source_branch']).to be_truthy
       end
@@ -705,7 +712,7 @@ describe API::MergeRequests, api: true  do
   describe 'Time tracking' do
     let(:issuable) { merge_request }
 
-    include_examples 'time tracking endpoints', 'merge_request'
+    include_examples 'V3 time tracking endpoints', 'merge_request'
   end
 
   def mr_with_later_created_and_updated_at_time
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..127c0eec88197fc2fdc048b8b83cc1e2c2204fc0
--- /dev/null
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -0,0 +1,239 @@
+require 'spec_helper'
+
+describe API::V3::Milestones, api: true  do
+  include ApiHelpers
+  let(:user) { create(:user) }
+  let!(:project) { create(:empty_project, namespace: user.namespace ) }
+  let!(:closed_milestone) { create(:closed_milestone, project: project) }
+  let!(:milestone) { create(:milestone, project: project) }
+
+  before { project.team << [user, :developer] }
+
+  describe 'GET /projects/:id/milestones' do
+    it 'returns project milestones' do
+      get v3_api("/projects/#{project.id}/milestones", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.first['title']).to eq(milestone.title)
+    end
+
+    it 'returns a 401 error if user not authenticated' do
+      get v3_api("/projects/#{project.id}/milestones")
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns an array of active milestones' do
+      get v3_api("/projects/#{project.id}/milestones?state=active", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(1)
+      expect(json_response.first['id']).to eq(milestone.id)
+    end
+
+    it 'returns an array of closed milestones' do
+      get v3_api("/projects/#{project.id}/milestones?state=closed", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(1)
+      expect(json_response.first['id']).to eq(closed_milestone.id)
+    end
+  end
+
+  describe 'GET /projects/:id/milestones/:milestone_id' do
+    it 'returns a project milestone by id' do
+      get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['title']).to eq(milestone.title)
+      expect(json_response['iid']).to eq(milestone.iid)
+    end
+
+    it 'returns a project milestone by iid' do
+      get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
+
+      expect(response.status).to eq 200
+      expect(json_response.size).to eq(1)
+      expect(json_response.first['title']).to eq closed_milestone.title
+      expect(json_response.first['id']).to eq closed_milestone.id
+    end
+
+    it 'returns a project milestone by iid array' do
+      get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+
+      expect(response).to have_http_status(200)
+      expect(json_response.size).to eq(2)
+      expect(json_response.first['title']).to eq milestone.title
+      expect(json_response.first['id']).to eq milestone.id
+    end
+
+    it 'returns 401 error if user not authenticated' do
+      get v3_api("/projects/#{project.id}/milestones/#{milestone.id}")
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 404 error if milestone id not found' do
+      get v3_api("/projects/#{project.id}/milestones/1234", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'POST /projects/:id/milestones' do
+    it 'creates a new project milestone' do
+      post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('new milestone')
+      expect(json_response['description']).to be_nil
+    end
+
+    it 'creates a new project milestone with description and dates' do
+      post v3_api("/projects/#{project.id}/milestones", user),
+        title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['description']).to eq('release')
+      expect(json_response['due_date']).to eq('2013-03-02')
+      expect(json_response['start_date']).to eq('2013-02-02')
+    end
+
+    it 'returns a 400 error if title is missing' do
+      post v3_api("/projects/#{project.id}/milestones", user)
+
+      expect(response).to have_http_status(400)
+    end
+
+    it 'returns a 400 error if params are invalid (duplicate title)' do
+      post v3_api("/projects/#{project.id}/milestones", user),
+        title: milestone.title, description: 'release', due_date: '2013-03-02'
+
+      expect(response).to have_http_status(400)
+    end
+
+    it 'creates a new project with reserved html characters' do
+      post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
+      expect(json_response['description']).to be_nil
+    end
+  end
+
+  describe 'PUT /projects/:id/milestones/:milestone_id' do
+    it 'updates a project milestone' do
+      put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+        title: 'updated title'
+
+      expect(response).to have_http_status(200)
+      expect(json_response['title']).to eq('updated title')
+    end
+
+    it 'removes a due date if nil is passed' do
+      milestone.update!(due_date: "2016-08-05")
+
+      put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+
+      expect(response).to have_http_status(200)
+      expect(json_response['due_date']).to be_nil
+    end
+
+    it 'returns a 404 error if milestone id not found' do
+      put v3_api("/projects/#{project.id}/milestones/1234", user),
+        title: 'updated title'
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
+    it 'updates a project milestone' do
+      put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+        state_event: 'close'
+      expect(response).to have_http_status(200)
+
+      expect(json_response['state']).to eq('closed')
+    end
+  end
+
+  describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
+    it 'creates an activity event when an milestone is closed' do
+      expect(Event).to receive(:create)
+
+      put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+          state_event: 'close'
+    end
+  end
+
+  describe 'GET /projects/:id/milestones/:milestone_id/issues' do
+    before do
+      milestone.issues << create(:issue, project: project)
+    end
+    it 'returns project issues for a particular milestone' do
+      get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.first['milestone']['title']).to eq(milestone.title)
+    end
+
+    it 'matches V3 response schema for a list of issues' do
+      get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+      expect(response).to have_http_status(200)
+      expect(response).to match_response_schema('public_api/v3/issues')
+    end
+
+    it 'returns a 401 error if user not authenticated' do
+      get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+
+      expect(response).to have_http_status(401)
+    end
+
+    describe 'confidential issues' do
+      let(:public_project) { create(:empty_project, :public) }
+      let(:milestone) { create(:milestone, project: public_project) }
+      let(:issue) { create(:issue, project: public_project) }
+      let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
+      before do
+        public_project.team << [user, :developer]
+        milestone.issues << issue << confidential_issue
+      end
+
+      it 'returns confidential issues to team members' do
+        get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(2)
+        expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
+      end
+
+      it 'does not return confidential issues to team members with guest role' do
+        member = create(:user)
+        project.team << [member, :guest]
+
+        get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(1)
+        expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+      end
+
+      it 'does not return confidential issues to regular users' do
+        get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(1)
+        expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddef2d5eb04a0a342433c455e4073741653b1dba
--- /dev/null
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -0,0 +1,433 @@
+require 'spec_helper'
+
+describe API::V3::Notes, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
+  let!(:issue) { create(:issue, project: project, author: user) }
+  let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+  let!(:snippet) { create(:project_snippet, project: project, author: user) }
+  let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
+  let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
+  let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
+
+  # For testing the cross-reference of a private issue in a public issue
+  let(:private_user)    { create(:user) }
+  let(:private_project) do
+    create(:empty_project, namespace: private_user.namespace).
+    tap { |p| p.team << [private_user, :master] }
+  end
+  let(:private_issue)    { create(:issue, project: private_project) }
+
+  let(:ext_proj)  { create(:empty_project, :public) }
+  let(:ext_issue) { create(:issue, project: ext_proj) }
+
+  let!(:cross_reference_note) do
+    create :note,
+    noteable: ext_issue, project: ext_proj,
+    note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+    system: true
+  end
+
+  before { project.team << [user, :reporter] }
+
+  describe "GET /projects/:id/noteable/:noteable_id/notes" do
+    context "when noteable is an Issue" do
+      it "returns an array of issue notes" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.first['body']).to eq(issue_note.note)
+        expect(json_response.first['upvote']).to be_falsey
+        expect(json_response.first['downvote']).to be_falsey
+      end
+
+      it "returns a 404 error when issue id not found" do
+        get v3_api("/projects/#{project.id}/issues/12345/notes", user)
+
+        expect(response).to have_http_status(404)
+      end
+
+      context "and current user cannot view the notes" do
+        it "returns an empty array" do
+          get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+          expect(response).to have_http_status(200)
+          expect(response).to include_pagination_headers
+          expect(json_response).to be_an Array
+          expect(json_response).to be_empty
+        end
+
+        context "and issue is confidential" do
+          before { ext_issue.update_attributes(confidential: true) }
+
+          it "returns 404" do
+            get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context "and current user can view the note" do
+          it "returns an empty array" do
+            get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+
+            expect(response).to have_http_status(200)
+            expect(response).to include_pagination_headers
+            expect(json_response).to be_an Array
+            expect(json_response.first['body']).to eq(cross_reference_note.note)
+          end
+        end
+      end
+    end
+
+    context "when noteable is a Snippet" do
+      it "returns an array of snippet notes" do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.first['body']).to eq(snippet_note.note)
+      end
+
+      it "returns a 404 error when snippet id not found" do
+        get v3_api("/projects/#{project.id}/snippets/42/notes", user)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "returns 404 when not authorized" do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "when noteable is a Merge Request" do
+      it "returns an array of merge_requests notes" do
+        get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.first['body']).to eq(merge_request_note.note)
+      end
+
+      it "returns a 404 error if merge request id not found" do
+        get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user)
+
+        expect(response).to have_http_status(404)
+      end
+
+      it "returns 404 when not authorized" do
+        get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
+    context "when noteable is an Issue" do
+      it "returns an issue note by id" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['body']).to eq(issue_note.note)
+      end
+
+      it "returns a 404 error if issue note not found" do
+        get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+
+      context "and current user cannot view the note" do
+        it "returns a 404 error" do
+          get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+
+          expect(response).to have_http_status(404)
+        end
+
+        context "when issue is confidential" do
+          before { issue.update_attributes(confidential: true) }
+
+          it "returns 404" do
+            get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context "and current user can view the note" do
+          it "returns an issue note by id" do
+            get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+
+            expect(response).to have_http_status(200)
+            expect(json_response['body']).to eq(cross_reference_note.note)
+          end
+        end
+      end
+    end
+
+    context "when noteable is a Snippet" do
+      it "returns a snippet note by id" do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['body']).to eq(snippet_note.note)
+      end
+
+      it "returns a 404 error if snippet note not found" do
+        get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "POST /projects/:id/noteable/:noteable_id/notes" do
+    context "when noteable is an Issue" do
+      it "creates a new issue note" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+
+        expect(response).to have_http_status(201)
+        expect(json_response['body']).to eq('hi!')
+        expect(json_response['author']['username']).to eq(user.username)
+      end
+
+      it "returns a 400 bad request error if body not given" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+        expect(response).to have_http_status(400)
+      end
+
+      it "returns a 401 unauthorized error if user not authenticated" do
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
+
+        expect(response).to have_http_status(401)
+      end
+
+      context 'when an admin or owner makes the request' do
+        it 'accepts the creation date to be set' do
+          creation_time = 2.weeks.ago
+          post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+            body: 'hi!', created_at: creation_time
+
+          expect(response).to have_http_status(201)
+          expect(json_response['body']).to eq('hi!')
+          expect(json_response['author']['username']).to eq(user.username)
+          expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+        end
+      end
+
+      context 'when the user is posting an award emoji on an issue created by someone else' do
+        let(:issue2) { create(:issue, project: project) }
+
+        it 'creates a new issue note' do
+          post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
+
+          expect(response).to have_http_status(201)
+          expect(json_response['body']).to eq(':+1:')
+        end
+      end
+
+      context 'when the user is posting an award emoji on his/her own issue' do
+        it 'creates a new issue note' do
+          post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+
+          expect(response).to have_http_status(201)
+          expect(json_response['body']).to eq(':+1:')
+        end
+      end
+    end
+
+    context "when noteable is a Snippet" do
+      it "creates a new snippet note" do
+        post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
+
+        expect(response).to have_http_status(201)
+        expect(json_response['body']).to eq('hi!')
+        expect(json_response['author']['username']).to eq(user.username)
+      end
+
+      it "returns a 400 bad request error if body not given" do
+        post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+        expect(response).to have_http_status(400)
+      end
+
+      it "returns a 401 unauthorized error if user not authenticated" do
+        post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when user does not have access to read the noteable' do
+      it 'responds with 404' do
+        project = create(:empty_project, :private) { |p| p.add_guest(user) }
+        issue = create(:issue, :confidential, project: project)
+
+        post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+          body: 'Foo'
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when user does not have access to create noteable' do
+      let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
+
+      ##
+      # We are posting to project user has access to, but we use issue id
+      # from a different project, see #15577
+      #
+      before do
+        post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user),
+             body: 'Hi!'
+      end
+
+      it 'responds with resource not found error' do
+        expect(response.status).to eq 404
+      end
+
+      it 'does not create new note' do
+        expect(private_issue.notes.reload).to be_empty
+      end
+    end
+  end
+
+  describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
+    it "creates an activity event when an issue note is created" do
+      expect(Event).to receive(:create)
+
+      post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+    end
+  end
+
+  describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
+    context 'when noteable is an Issue' do
+      it 'returns modified note' do
+        put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+                  "notes/#{issue_note.id}", user), body: 'Hello!'
+
+        expect(response).to have_http_status(200)
+        expect(json_response['body']).to eq('Hello!')
+      end
+
+      it 'returns a 404 error when note id not found' do
+        put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
+                body: 'Hello!'
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a 400 bad request error if body not given' do
+        put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+                  "notes/#{issue_note.id}", user)
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context 'when noteable is a Snippet' do
+      it 'returns modified note' do
+        put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                  "notes/#{snippet_note.id}", user), body: 'Hello!'
+
+        expect(response).to have_http_status(200)
+        expect(json_response['body']).to eq('Hello!')
+      end
+
+      it 'returns a 404 error when note id not found' do
+        put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                  "notes/12345", user), body: "Hello!"
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when noteable is a Merge Request' do
+      it 'returns modified note' do
+        put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+                  "notes/#{merge_request_note.id}", user), body: 'Hello!'
+
+        expect(response).to have_http_status(200)
+        expect(json_response['body']).to eq('Hello!')
+      end
+
+      it 'returns a 404 error when note id not found' do
+        put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+                  "notes/12345", user), body: "Hello!"
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
+    context 'when noteable is an Issue' do
+      it 'deletes a note' do
+        delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+                      "notes/#{issue_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+                      "notes/#{issue_note.id}", user)
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a 404 error when note id not found' do
+        delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when noteable is a Snippet' do
+      it 'deletes a note' do
+        delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                      "notes/#{snippet_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                      "notes/#{snippet_note.id}", user)
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a 404 error when note id not found' do
+        delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                      "notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when noteable is a Merge Request' do
+      it 'deletes a note' do
+        delete v3_api("/projects/#{project.id}/merge_requests/"\
+                      "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete v3_api("/projects/#{project.id}/merge_requests/"\
+                      "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a 404 error when note id not found' do
+        delete v3_api("/projects/#{project.id}/merge_requests/"\
+                      "#{merge_request.id}/notes/12345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3786eb0693247aef33c783be6cc4e672def2056a
--- /dev/null
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -0,0 +1,203 @@
+require 'spec_helper'
+
+describe API::V3::Pipelines, api: true do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:non_member)  { create(:user) }
+  let(:project)     { create(:project, :repository, creator: user) }
+
+  let!(:pipeline) do
+    create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+                               ref: project.default_branch)
+  end
+
+  before { project.team << [user, :master] }
+
+  shared_examples 'a paginated resources' do
+    before do
+      # Fires the request
+      request
+    end
+
+    it 'has pagination headers' do
+      expect(response).to include_pagination_headers
+    end
+  end
+
+  describe 'GET /projects/:id/pipelines ' do
+    it_behaves_like 'a paginated resources' do
+      let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) }
+    end
+
+    context 'authorized user' do
+      it 'returns project pipelines' do
+        get v3_api("/projects/#{project.id}/pipelines", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['sha']).to match(/\A\h{40}\z/)
+        expect(json_response.first['id']).to eq pipeline.id
+        expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage])
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not return project pipelines' do
+        get v3_api("/projects/#{project.id}/pipelines", non_member)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq '404 Project Not Found'
+        expect(json_response).not_to be_an Array
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/pipeline ' do
+    context 'authorized user' do
+      context 'with gitlab-ci.yml' do
+        before { stub_ci_pipeline_to_return_yaml_file }
+
+        it 'creates and returns a new pipeline' do
+          expect do
+            post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+          end.to change { Ci::Pipeline.count }.by(1)
+
+          expect(response).to have_http_status(201)
+          expect(json_response).to be_a Hash
+          expect(json_response['sha']).to eq project.commit.id
+        end
+
+        it 'fails when using an invalid ref' do
+          post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
+
+          expect(response).to have_http_status(400)
+          expect(json_response['message']['base'].first).to eq 'Reference not found'
+          expect(json_response).not_to be_an Array
+        end
+      end
+
+      context 'without gitlab-ci.yml' do
+        it 'fails to create pipeline' do
+          post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+
+          expect(response).to have_http_status(400)
+          expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
+          expect(json_response).not_to be_an Array
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not create pipeline' do
+        post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq '404 Project Not Found'
+        expect(json_response).not_to be_an Array
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/pipelines/:pipeline_id' do
+    context 'authorized user' do
+      it 'returns project pipelines' do
+        get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['sha']).to match /\A\h{40}\z/
+      end
+
+      it 'returns 404 when it does not exist' do
+        get v3_api("/projects/#{project.id}/pipelines/123456", user)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq '404 Not found'
+        expect(json_response['id']).to be nil
+      end
+
+      context 'with coverage' do
+        before do
+          create(:ci_build, coverage: 30, pipeline: pipeline)
+        end
+
+        it 'exposes the coverage' do
+          get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+          expect(json_response["coverage"].to_i).to eq(30)
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not return a project pipeline' do
+        get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq '404 Project Not Found'
+        expect(json_response['id']).to be nil
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
+    context 'authorized user' do
+      let!(:pipeline) do
+        create(:ci_pipeline, project: project, sha: project.commit.id,
+                             ref: project.default_branch)
+      end
+
+      let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+      it 'retries failed builds' do
+        expect do
+          post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+        end.to change { pipeline.builds.count }.from(1).to(2)
+
+        expect(response).to have_http_status(201)
+        expect(build.reload.retried?).to be true
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not return a project pipeline' do
+        post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq '404 Project Not Found'
+        expect(json_response['id']).to be nil
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
+    let!(:pipeline) do
+      create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+                                 ref: project.default_branch)
+    end
+
+    let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+    context 'authorized user' do
+      it 'retries failed builds' do
+        post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['status']).to eq('canceled')
+      end
+    end
+
+    context 'user without proper access rights' do
+      let!(:reporter) { create(:user) }
+
+      before { project.team << [reporter, :reporter] }
+
+      it 'rejects the action' do
+        post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
+
+        expect(response).to have_http_status(403)
+        expect(pipeline.reload.status).to eq('pending')
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a981119dc5ada2e6da0ea284a8f19fc93f4164fb
--- /dev/null
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -0,0 +1,216 @@
+require 'spec_helper'
+
+describe API::ProjectHooks, 'ProjectHooks', api: true do
+  include ApiHelpers
+  let(:user) { create(:user) }
+  let(:user3) { create(:user) }
+  let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+  let!(:hook) do
+    create(:project_hook,
+           :all_events_enabled,
+           project: project,
+           url: 'http://example.com',
+           enable_ssl_verification: true)
+  end
+
+  before do
+    project.team << [user, :master]
+    project.team << [user3, :developer]
+  end
+
+  describe "GET /projects/:id/hooks" do
+    context "authorized user" do
+      it "returns project hooks" do
+        get v3_api("/projects/#{project.id}/hooks", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.count).to eq(1)
+        expect(json_response.first['url']).to eq("http://example.com")
+        expect(json_response.first['issues_events']).to eq(true)
+        expect(json_response.first['push_events']).to eq(true)
+        expect(json_response.first['merge_requests_events']).to eq(true)
+        expect(json_response.first['tag_push_events']).to eq(true)
+        expect(json_response.first['note_events']).to eq(true)
+        expect(json_response.first['build_events']).to eq(true)
+        expect(json_response.first['pipeline_events']).to eq(true)
+        expect(json_response.first['wiki_page_events']).to eq(true)
+        expect(json_response.first['enable_ssl_verification']).to eq(true)
+      end
+    end
+
+    context "unauthorized user" do
+      it "does not access project hooks" do
+        get v3_api("/projects/#{project.id}/hooks", user3)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+  end
+
+  describe "GET /projects/:id/hooks/:hook_id" do
+    context "authorized user" do
+      it "returns a project hook" do
+        get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+        expect(response).to have_http_status(200)
+        expect(json_response['url']).to eq(hook.url)
+        expect(json_response['issues_events']).to eq(hook.issues_events)
+        expect(json_response['push_events']).to eq(hook.push_events)
+        expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+        expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+        expect(json_response['note_events']).to eq(hook.note_events)
+        expect(json_response['build_events']).to eq(hook.build_events)
+        expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+        expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+        expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+      end
+
+      it "returns a 404 error if hook id is not available" do
+        get v3_api("/projects/#{project.id}/hooks/1234", user)
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "unauthorized user" do
+      it "does not access an existing hook" do
+        get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3)
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    it "returns a 404 error if hook id is not available" do
+      get v3_api("/projects/#{project.id}/hooks/1234", user)
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe "POST /projects/:id/hooks" do
+    it "adds hook to project" do
+      expect do
+        post v3_api("/projects/#{project.id}/hooks", user),
+          url: "http://example.com", issues_events: true, wiki_page_events: true
+      end.to change {project.hooks.count}.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response['url']).to eq('http://example.com')
+      expect(json_response['issues_events']).to eq(true)
+      expect(json_response['push_events']).to eq(true)
+      expect(json_response['merge_requests_events']).to eq(false)
+      expect(json_response['tag_push_events']).to eq(false)
+      expect(json_response['note_events']).to eq(false)
+      expect(json_response['build_events']).to eq(false)
+      expect(json_response['pipeline_events']).to eq(false)
+      expect(json_response['wiki_page_events']).to eq(true)
+      expect(json_response['enable_ssl_verification']).to eq(true)
+      expect(json_response).not_to include('token')
+    end
+
+    it "adds the token without including it in the response" do
+      token = "secret token"
+
+      expect do
+        post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
+      end.to change {project.hooks.count}.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response["url"]).to eq("http://example.com")
+      expect(json_response).not_to include("token")
+
+      hook = project.hooks.find(json_response["id"])
+
+      expect(hook.url).to eq("http://example.com")
+      expect(hook.token).to eq(token)
+    end
+
+    it "returns a 400 error if url not given" do
+      post v3_api("/projects/#{project.id}/hooks", user)
+      expect(response).to have_http_status(400)
+    end
+
+    it "returns a 422 error if url not valid" do
+      post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
+      expect(response).to have_http_status(422)
+    end
+  end
+
+  describe "PUT /projects/:id/hooks/:hook_id" do
+    it "updates an existing project hook" do
+      put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user),
+        url: 'http://example.org', push_events: false
+      expect(response).to have_http_status(200)
+      expect(json_response['url']).to eq('http://example.org')
+      expect(json_response['issues_events']).to eq(hook.issues_events)
+      expect(json_response['push_events']).to eq(false)
+      expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+      expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+      expect(json_response['note_events']).to eq(hook.note_events)
+      expect(json_response['build_events']).to eq(hook.build_events)
+      expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+      expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+      expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+    end
+
+    it "adds the token without including it in the response" do
+      token = "secret token"
+
+      put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
+
+      expect(response).to have_http_status(200)
+      expect(json_response["url"]).to eq("http://example.org")
+      expect(json_response).not_to include("token")
+
+      expect(hook.reload.url).to eq("http://example.org")
+      expect(hook.reload.token).to eq(token)
+    end
+
+    it "returns 404 error if hook id not found" do
+      put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 400 error if url is not given" do
+      put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      expect(response).to have_http_status(400)
+    end
+
+    it "returns a 422 error if url is not valid" do
+      put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
+      expect(response).to have_http_status(422)
+    end
+  end
+
+  describe "DELETE /projects/:id/hooks/:hook_id" do
+    it "deletes hook from project" do
+      expect do
+        delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      end.to change {project.hooks.count}.by(-1)
+      expect(response).to have_http_status(200)
+    end
+
+    it "returns success when deleting hook" do
+      delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      expect(response).to have_http_status(200)
+    end
+
+    it "returns a 404 error when deleting non existent hook" do
+      delete v3_api("/projects/#{project.id}/hooks/42", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns a 404 error if hook id not given" do
+      delete v3_api("/projects/#{project.id}/hooks", user)
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
+      test_user = create(:user)
+      other_project = create(:project)
+      other_project.team << [test_user, :master]
+
+      delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
+      expect(response).to have_http_status(404)
+      expect(WebHook.exists?(hook.id)).to be_truthy
+    end
+  end
+end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 36d99d80e798ccca032aa5af5ce9590d49db6ae1..d8bb562587d30096f0d51899fe4ea76dd2cceeef 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -84,7 +84,7 @@ describe API::V3::Projects, api: true do
 
       context 'GET /projects?simple=true' do
         it 'returns a simplified version of all the projects' do
-          expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+          expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
 
           get v3_api('/projects?simple=true', user)
 
@@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do
       end
     end
 
-    it 'creates new project without path and return 201' do
-      expect { post v3_api('/projects', user), name: 'foo' }.
+    it 'creates new project without path but with name and returns 201' do
+      expect { post v3_api('/projects', user), name: 'Foo Project' }.
         to change { Project.count }.by(1)
       expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('Foo Project')
+      expect(project.path).to eq('foo-project')
+    end
+
+    it 'creates new project without name but with path and returns 201' do
+      expect { post v3_api('/projects', user), path: 'foo_project' }.
+        to change { Project.count }.by(1)
+      expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('foo_project')
+      expect(project.path).to eq('foo_project')
+    end
+
+    it 'creates new project name and path and returns 201' do
+      expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+        to change { Project.count }.by(1)
+      expect(response).to have_http_status(201)
+
+      project = Project.first
+
+      expect(project.name).to eq('Foo Project')
+      expect(project.path).to eq('foo-Project')
     end
 
     it 'creates last project before reaching project limit' do
@@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do
       expect(response).to have_http_status(201)
     end
 
-    it 'does not create new project without name and return 400' do
+    it 'does not create new project without name or path and return 400' do
       expect { post v3_api('/projects', user) }.not_to change { Project.count }
       expect(response).to have_http_status(400)
     end
@@ -400,7 +427,7 @@ describe API::V3::Projects, api: true do
       expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
     end
 
-    it 'sets a project as allowing merge only if build succeeds' do
+    it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
       project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
       post v3_api('/projects', user), project
       expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
@@ -545,7 +572,7 @@ describe API::V3::Projects, api: true do
       expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
     end
 
-    it 'sets a project as allowing merge only if build succeeds' do
+    it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
       project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
       post v3_api("/projects/user/#{user.id}", admin), project
       expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
@@ -642,7 +669,7 @@ describe API::V3::Projects, api: true do
         expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
         expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
         expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
-        expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+        expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
         expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
       end
 
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index c696721c1c9177e8f6919606f03f246a5955219b..fef6fb641fa72e4f52da324f9d5be25c535b762f 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -3,6 +3,8 @@ require 'mime/types'
 
 describe API::V3::Repositories, api: true  do
   include ApiHelpers
+  include RepoHelpers
+  include WorkhorseHelpers
 
   let(:user) { create(:user) }
   let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
@@ -96,6 +98,226 @@ describe API::V3::Repositories, api: true  do
     end
   end
 
+  {
+    'blobs/:sha' => 'blobs/master',
+    'commits/:sha/blob' => 'commits/master/blob'
+  }.each do |desc_path, example_path|
+    describe "GET /projects/:id/repository/#{desc_path}" do
+      let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+      shared_examples_for 'repository blob' do
+        it 'returns the repository blob' do
+          get v3_api(route, current_user)
+          expect(response).to have_http_status(200)
+        end
+        context 'when sha does not exist' do
+          it_behaves_like '404 response' do
+            let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) }
+            let(:message) { '404 Commit Not Found' }
+          end
+        end
+        context 'when filepath does not exist' do
+          it_behaves_like '404 response' do
+            let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) }
+            let(:message) { '404 File Not Found' }
+          end
+        end
+        context 'when no filepath is given' do
+          it_behaves_like '400 response' do
+            let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) }
+          end
+        end
+        context 'when repository is disabled' do
+          include_context 'disabled repository'
+          it_behaves_like '403 response' do
+            let(:request) { get v3_api(route, current_user) }
+          end
+        end
+      end
+      context 'when unauthenticated', 'and project is public' do
+        it_behaves_like 'repository blob' do
+          let(:project) { create(:project, :public, :repository) }
+          let(:current_user) { nil }
+        end
+      end
+      context 'when unauthenticated', 'and project is private' do
+        it_behaves_like '404 response' do
+          let(:request) { get v3_api(route) }
+          let(:message) { '404 Project Not Found' }
+        end
+      end
+      context 'when authenticated', 'as a developer' do
+        it_behaves_like 'repository blob' do
+          let(:current_user) { user }
+        end
+      end
+      context 'when authenticated', 'as a guest' do
+        it_behaves_like '403 response' do
+          let(:request) { get v3_api(route, guest) }
+        end
+      end
+    end
+  end
+  describe "GET /projects/:id/repository/raw_blobs/:sha" do
+    let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+    shared_examples_for 'repository raw blob' do
+      it 'returns the repository raw blob' do
+        get v3_api(route, current_user)
+        expect(response).to have_http_status(200)
+      end
+      context 'when sha does not exist' do
+        it_behaves_like '404 response' do
+          let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) }
+          let(:message) { '404 Blob Not Found' }
+        end
+      end
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+        it_behaves_like '403 response' do
+          let(:request) { get v3_api(route, current_user) }
+        end
+      end
+    end
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository raw blob' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get v3_api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository raw blob' do
+        let(:current_user) { user }
+      end
+    end
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get v3_api(route, guest) }
+      end
+    end
+  end
+  describe "GET /projects/:id/repository/archive(.:format)?:sha" do
+    let(:route) { "/projects/#{project.id}/repository/archive" }
+    shared_examples_for 'repository archive' do
+      it 'returns the repository archive' do
+        get v3_api(route, current_user)
+        expect(response).to have_http_status(200)
+        repo_name = project.repository.name.gsub("\.git", "")
+        type, params = workhorse_send_data
+        expect(type).to eq('git-archive')
+        expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+      end
+      it 'returns the repository archive archive.zip' do
+        get v3_api("/projects/#{project.id}/repository/archive.zip", user)
+        expect(response).to have_http_status(200)
+        repo_name = project.repository.name.gsub("\.git", "")
+        type, params = workhorse_send_data
+        expect(type).to eq('git-archive')
+        expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+      end
+      it 'returns the repository archive archive.tar.bz2' do
+        get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user)
+        expect(response).to have_http_status(200)
+        repo_name = project.repository.name.gsub("\.git", "")
+        type, params = workhorse_send_data
+        expect(type).to eq('git-archive')
+        expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+      end
+      context 'when sha does not exist' do
+        it_behaves_like '404 response' do
+          let(:request) { get v3_api("#{route}?sha=xxx", current_user) }
+          let(:message) { '404 File Not Found' }
+        end
+      end
+    end
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository archive' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get v3_api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository archive' do
+        let(:current_user) { user }
+      end
+    end
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get v3_api(route, guest) }
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/repository/compare' do
+    let(:route) { "/projects/#{project.id}/repository/compare" }
+    shared_examples_for 'repository compare' do
+      it "compares branches" do
+        get v3_api(route, current_user), from: 'master', to: 'feature'
+        expect(response).to have_http_status(200)
+        expect(json_response['commits']).to be_present
+        expect(json_response['diffs']).to be_present
+      end
+      it "compares tags" do
+        get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
+        expect(response).to have_http_status(200)
+        expect(json_response['commits']).to be_present
+        expect(json_response['diffs']).to be_present
+      end
+      it "compares commits" do
+        get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
+        expect(response).to have_http_status(200)
+        expect(json_response['commits']).to be_empty
+        expect(json_response['diffs']).to be_empty
+        expect(json_response['compare_same_ref']).to be_falsey
+      end
+      it "compares commits in reverse order" do
+        get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
+        expect(response).to have_http_status(200)
+        expect(json_response['commits']).to be_present
+        expect(json_response['diffs']).to be_present
+      end
+      it "compares same refs" do
+        get v3_api(route, current_user), from: 'master', to: 'master'
+        expect(response).to have_http_status(200)
+        expect(json_response['commits']).to be_empty
+        expect(json_response['diffs']).to be_empty
+        expect(json_response['compare_same_ref']).to be_truthy
+      end
+    end
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository compare' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get v3_api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository compare' do
+        let(:current_user) { user }
+      end
+    end
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get v3_api(route, guest) }
+      end
+    end
+  end
+
   describe 'GET /projects/:id/repository/contributors' do
     let(:route) { "/projects/#{project.id}/repository/contributors" }
 
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca335ce9cf012d0d4d833e4cd98e794c5736f79a
--- /dev/null
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe API::V3::Runners, api: true  do
+  include ApiHelpers
+
+  let(:admin) { create(:user, :admin) }
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+
+  let(:project) { create(:empty_project, creator_id: user.id) }
+  let(:project2) { create(:empty_project, creator_id: user.id) }
+
+  let!(:shared_runner) { create(:ci_runner, :shared) }
+  let!(:unused_specific_runner) { create(:ci_runner) }
+
+  let!(:specific_runner) do
+    create(:ci_runner).tap do |runner|
+      create(:ci_runner_project, runner: runner, project: project)
+    end
+  end
+
+  let!(:two_projects_runner) do
+    create(:ci_runner).tap do |runner|
+      create(:ci_runner_project, runner: runner, project: project)
+      create(:ci_runner_project, runner: runner, project: project2)
+    end
+  end
+
+  before do
+    # Set project access for users
+    create(:project_member, :master, user: user, project: project)
+    create(:project_member, :reporter, user: user2, project: project)
+  end
+
+  describe 'DELETE /runners/:id' do
+    context 'admin user' do
+      context 'when runner is shared' do
+        it 'deletes runner' do
+          expect do
+            delete v3_api("/runners/#{shared_runner.id}", admin)
+
+            expect(response).to have_http_status(200)
+          end.to change{ Ci::Runner.shared.count }.by(-1)
+        end
+      end
+
+      context 'when runner is not shared' do
+        it 'deletes unused runner' do
+          expect do
+            delete v3_api("/runners/#{unused_specific_runner.id}", admin)
+
+            expect(response).to have_http_status(200)
+          end.to change{ Ci::Runner.specific.count }.by(-1)
+        end
+
+        it 'deletes used runner' do
+          expect do
+            delete v3_api("/runners/#{specific_runner.id}", admin)
+
+            expect(response).to have_http_status(200)
+          end.to change{ Ci::Runner.specific.count }.by(-1)
+        end
+      end
+
+      it 'returns 404 if runner does not exists' do
+        delete v3_api('/runners/9999', admin)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authorized user' do
+      context 'when runner is shared' do
+        it 'does not delete runner' do
+          delete v3_api("/runners/#{shared_runner.id}", user)
+          expect(response).to have_http_status(403)
+        end
+      end
+
+      context 'when runner is not shared' do
+        it 'does not delete runner without access to it' do
+          delete v3_api("/runners/#{specific_runner.id}", user2)
+          expect(response).to have_http_status(403)
+        end
+
+        it 'does not delete runner with more than one associated project' do
+          delete v3_api("/runners/#{two_projects_runner.id}", user)
+          expect(response).to have_http_status(403)
+        end
+
+        it 'deletes runner for one owned project' do
+          expect do
+            delete v3_api("/runners/#{specific_runner.id}", user)
+
+            expect(response).to have_http_status(200)
+          end.to change{ Ci::Runner.specific.count }.by(-1)
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not delete runner' do
+        delete v3_api("/runners/#{specific_runner.id}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/runners/:runner_id' do
+    context 'authorized user' do
+      context 'when runner have more than one associated projects' do
+        it "disables project's runner" do
+          expect do
+            delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+            expect(response).to have_http_status(200)
+          end.to change{ project.runners.count }.by(-1)
+        end
+      end
+
+      context 'when runner have one associated projects' do
+        it "does not disable project's runner" do
+          expect do
+            delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
+          end.to change{ project.runners.count }.by(0)
+          expect(response).to have_http_status(403)
+        end
+      end
+
+      it 'returns 404 is runner is not found' do
+        delete v3_api("/projects/#{project.id}/runners/9999", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authorized user without permissions' do
+      it "does not disable project's runner" do
+        delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it "does not disable project's runner" do
+        delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a760a8f25c680302807f4c156dd5a436b018265
--- /dev/null
+++ b/spec/requests/api/v3/services_spec.rb
@@ -0,0 +1,24 @@
+require "spec_helper"
+
+describe API::V3::Services, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+
+  available_services = Service.available_services_names
+  available_services.delete('prometheus')
+  available_services.each do |service|
+    describe "DELETE /projects/:id/services/#{service.dasherize}" do
+      include_context service
+
+      it "deletes #{service}" do
+        delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user)
+
+        expect(response).to have_http_status(200)
+        project.send(service_method).reload
+        expect(project.send(service_method).activated?).to be_falsey
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9fa5adac178e96ef8b28130c9040fca3c464ccf
--- /dev/null
+++ b/spec/requests/api/v3/settings_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe API::V3::Settings, 'Settings', api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:admin) { create(:admin) }
+
+  describe "GET /application/settings" do
+    it "returns application settings" do
+      get v3_api("/application/settings", admin)
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Hash
+      expect(json_response['default_projects_limit']).to eq(42)
+      expect(json_response['signin_enabled']).to be_truthy
+      expect(json_response['repository_storage']).to eq('default')
+      expect(json_response['koding_enabled']).to be_falsey
+      expect(json_response['koding_url']).to be_nil
+      expect(json_response['plantuml_enabled']).to be_falsey
+      expect(json_response['plantuml_url']).to be_nil
+    end
+  end
+
+  describe "PUT /application/settings" do
+    context "custom repository storage type set in the config" do
+      before do
+        storages = { 'custom' => 'tmp/tests/custom_repositories' }
+        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+      end
+
+      it "updates application settings" do
+        put v3_api("/application/settings", admin),
+          default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
+          plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
+        expect(response).to have_http_status(200)
+        expect(json_response['default_projects_limit']).to eq(3)
+        expect(json_response['signin_enabled']).to be_falsey
+        expect(json_response['repository_storage']).to eq('custom')
+        expect(json_response['repository_storages']).to eq(['custom'])
+        expect(json_response['koding_enabled']).to be_truthy
+        expect(json_response['koding_url']).to eq('http://koding.example.com')
+        expect(json_response['plantuml_enabled']).to be_truthy
+        expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+      end
+    end
+
+    context "missing koding_url value when koding_enabled is true" do
+      it "returns a blank parameter error message" do
+        put v3_api("/application/settings", admin), koding_enabled: true
+
+        expect(response).to have_http_status(400)
+        expect(json_response['error']).to eq('koding_url is missing')
+      end
+    end
+
+    context "missing plantuml_url value when plantuml_enabled is true" do
+      it "returns a blank parameter error message" do
+        put v3_api("/application/settings", admin), plantuml_enabled: true
+
+        expect(response).to have_http_status(400)
+        expect(json_response['error']).to eq('plantuml_url is missing')
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..05653bd0d51df27958368fec6b83dbe667972ab7
--- /dev/null
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -0,0 +1,187 @@
+require 'rails_helper'
+
+describe API::V3::Snippets, api: true do
+  include ApiHelpers
+  let!(:user) { create(:user) }
+
+  describe 'GET /snippets/' do
+    it 'returns snippets available' do
+      public_snippet = create(:personal_snippet, :public, author: user)
+      private_snippet = create(:personal_snippet, :private, author: user)
+      internal_snippet = create(:personal_snippet, :internal, author: user)
+
+      get v3_api("/snippets/", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+        public_snippet.id,
+        internal_snippet.id,
+        private_snippet.id)
+      expect(json_response.last).to have_key('web_url')
+      expect(json_response.last).to have_key('raw_url')
+    end
+
+    it 'hides private snippets from regular user' do
+      create(:personal_snippet, :private)
+
+      get v3_api("/snippets/", user)
+      expect(response).to have_http_status(200)
+      expect(json_response.size).to eq(0)
+    end
+  end
+
+  describe 'GET /snippets/public' do
+    let!(:other_user) { create(:user) }
+    let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+    let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+    let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+    let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+    let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+    it 'returns all snippets with public visibility from all users' do
+      get v3_api("/snippets/public", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+        public_snippet.id,
+        public_snippet_other.id)
+      expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+        "http://localhost/snippets/#{public_snippet.id}",
+        "http://localhost/snippets/#{public_snippet_other.id}")
+      expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+        "http://localhost/snippets/#{public_snippet.id}/raw",
+        "http://localhost/snippets/#{public_snippet_other.id}/raw")
+    end
+  end
+
+  describe 'GET /snippets/:id/raw' do
+    let(:snippet) { create(:personal_snippet, author: user) }
+
+    it 'returns raw text' do
+      get v3_api("/snippets/#{snippet.id}/raw", user)
+
+      expect(response).to have_http_status(200)
+      expect(response.content_type).to eq 'text/plain'
+      expect(response.body).to eq(snippet.content)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      delete v3_api("/snippets/1234", user)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+
+  describe 'POST /snippets/' do
+    let(:params) do
+      {
+        title: 'Test Title',
+        file_name: 'test.rb',
+        content: 'puts "hello world"',
+        visibility_level: Snippet::PUBLIC
+      }
+    end
+
+    it 'creates a new snippet' do
+      expect do
+        post v3_api("/snippets/", user), params
+      end.to change { PersonalSnippet.count }.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq(params[:title])
+      expect(json_response['file_name']).to eq(params[:file_name])
+    end
+
+    it 'returns 400 for missing parameters' do
+      params.delete(:title)
+
+      post v3_api("/snippets/", user), params
+
+      expect(response).to have_http_status(400)
+    end
+
+    context 'when the snippet is spam' do
+      def create_snippet(snippet_params = {})
+        post v3_api('/snippets', user), params.merge(snippet_params)
+      end
+
+      before do
+        allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+      end
+
+      context 'when the snippet is private' do
+        it 'creates the snippet' do
+          expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+            to change { Snippet.count }.by(1)
+        end
+      end
+
+      context 'when the snippet is public' do
+        it 'rejects the shippet' do
+          expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+            not_to change { Snippet.count }
+          expect(response).to have_http_status(400)
+        end
+
+        it 'creates a spam log' do
+          expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+            to change { SpamLog.count }.by(1)
+        end
+      end
+    end
+  end
+
+  describe 'PUT /snippets/:id' do
+    let(:other_user) { create(:user) }
+    let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    it 'updates snippet' do
+      new_content = 'New content'
+
+      put v3_api("/snippets/#{public_snippet.id}", user), content: new_content
+
+      expect(response).to have_http_status(200)
+      public_snippet.reload
+      expect(public_snippet.content).to eq(new_content)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      put v3_api("/snippets/1234", user), title: 'foo'
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+
+    it "returns 404 for another user's snippet" do
+      put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+
+    it 'returns 400 for missing parameters' do
+      put v3_api("/snippets/1234", user)
+
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  describe 'DELETE /snippets/:id' do
+    let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+    it 'deletes snippet' do
+      expect do
+        delete v3_api("/snippets/#{public_snippet.id}", user)
+
+        expect(response).to have_http_status(204)
+      end.to change { PersonalSnippet.count }.by(-1)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      delete v3_api("/snippets/1234", user)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index da58efb6ebf0de1a9fcab1dc89c98e4d0a26cc49..91038977c82998edcb95e4faf2d207e8684d49a0 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -38,4 +38,20 @@ describe API::V3::SystemHooks, api: true  do
       end
     end
   end
+
+  describe "DELETE /hooks/:id" do
+    it "deletes a hook" do
+      expect do
+        delete v3_api("/hooks/#{hook.id}", admin)
+
+        expect(response).to have_http_status(200)
+      end.to change { SystemHook.count }.by(-1)
+    end
+
+    it 'returns 404 if the system hook does not exist' do
+      delete v3_api('/hooks/12345', admin)
+
+      expect(response).to have_http_status(404)
+    end
+  end
 end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
index 6722789d928aac30d97f6be3adf4dfc432eeab08..6870cfd26683f4eeb79a352568c44d48dffa40d4 100644
--- a/spec/requests/api/v3/tags_spec.rb
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -64,4 +64,26 @@ describe API::V3::Tags, api: true  do
       end
     end
   end
+
+  describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+    let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+
+    before do
+      allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+    end
+
+    context 'delete tag' do
+      it 'deletes an existing tag' do
+        delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['tag_name']).to eq(tag_name)
+      end
+
+      it 'raises 404 if the tag does not exist' do
+        delete v3_api("/projects/#{project.id}/repository/tags/foobar", user)
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 4fd4e70beddbcd85b587b4e5fd33d676245cadf8..f1e554b98ccc95058c8d06673e7ce08a70d4c571 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -56,11 +56,11 @@ describe API::V3::Templates, api: true  do
       expect(json_response['popular']).to be true
       expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
       expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
-      expect(json_response['description']).to include('A permissive license that is short and to the point.')
+      expect(json_response['description']).to include('A short and simple permissive license with conditions')
       expect(json_response['conditions']).to eq(%w[include-copyright])
       expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
       expect(json_response['limitations']).to eq(%w[no-liability])
-      expect(json_response['content']).to include('The MIT License (MIT)')
+      expect(json_response['content']).to include('MIT License')
     end
   end
 
@@ -70,7 +70,7 @@ describe API::V3::Templates, api: true  do
 
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
-      expect(json_response.size).to eq(15)
+      expect(json_response.size).to eq(12)
       expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
     end
 
@@ -98,7 +98,7 @@ describe API::V3::Templates, api: true  do
         let(:license_type) { 'mit' }
 
         it 'returns the license text' do
-          expect(json_response['content']).to include('The MIT License (MIT)')
+          expect(json_response['content']).to include('MIT License')
         end
 
         it 'replaces placeholder values' do
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9233e9621bf51c05fa73343dfb0cc68f3b58b16a
--- /dev/null
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -0,0 +1,232 @@
+require 'spec_helper'
+
+describe API::V3::Triggers do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let!(:trigger_token) { 'secure_token' }
+  let!(:project) { create(:project, :repository, creator: user) }
+  let!(:master) { create(:project_member, :master, user: user, project: project) }
+  let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
+  let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+
+  describe 'POST /projects/:project_id/trigger' do
+    let!(:project2) { create(:project) }
+    let(:options) do
+      {
+        token: trigger_token
+      }
+    end
+
+    before do
+      stub_ci_pipeline_to_return_yaml_file
+    end
+
+    context 'Handles errors' do
+      it 'returns bad request if token is missing' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns not found if project is not found' do
+        post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns unauthorized if token is for different project' do
+        post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'Have a commit' do
+      let(:pipeline) { project.pipelines.last }
+
+      it 'creates builds' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+        expect(response).to have_http_status(201)
+        pipeline.builds.reload
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
+      end
+
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
+        post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
+        expect(response).to have_http_status(400)
+        expect(json_response['message']).to eq('No builds created')
+      end
+
+      context 'Validates variables' do
+        let(:variables) do
+          { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+        end
+
+        it 'validates variables to be a hash' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+          expect(response).to have_http_status(400)
+          expect(json_response['error']).to eq('variables is invalid')
+        end
+
+        it 'validates variables needs to be a map of key-valued strings' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+          expect(response).to have_http_status(400)
+          expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
+        end
+
+        it 'creates trigger request with variables' do
+          post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+          expect(response).to have_http_status(201)
+          pipeline.builds.reload
+          expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+        end
+      end
+    end
+
+    context 'when triggering a pipeline from a trigger token' do
+      it 'creates builds from the ref given in the URL, not in the body' do
+        expect do
+          post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+        end.to change(project.builds, :count).by(5)
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when ref contains a dot' do
+        it 'creates builds from the ref given in the URL, not in the body' do
+          project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+          expect do
+            post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+          end.to change(project.builds, :count).by(4)
+
+          expect(response).to have_http_status(201)
+        end
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/triggers' do
+    context 'authenticated user with valid permissions' do
+      it 'returns list of triggers' do
+        get v3_api("/projects/#{project.id}/triggers", user)
+
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_a(Array)
+        expect(json_response[0]).to have_key('token')
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/triggers/:token' do
+    context 'authenticated user with valid permissions' do
+      it 'returns trigger details' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_a(Hash)
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
+        get v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return triggers list' do
+        get v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/triggers' do
+    context 'authenticated user with valid permissions' do
+      it 'creates trigger' do
+        expect do
+          post v3_api("/projects/#{project.id}/triggers", user)
+        end.to change{project.triggers.count}.by(1)
+
+        expect(response).to have_http_status(201)
+        expect(json_response).to be_a(Hash)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not create trigger' do
+        post v3_api("/projects/#{project.id}/triggers", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not create trigger' do
+        post v3_api("/projects/#{project.id}/triggers")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/triggers/:token' do
+    context 'authenticated user with valid permissions' do
+      it 'deletes trigger' do
+        expect do
+          delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+          expect(response).to have_http_status(200)
+        end.to change{project.triggers.count}.by(-1)
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
+        delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not delete trigger' do
+        delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not delete trigger' do
+        delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index 5020ef18a3a8f7d568a962edd3f7fe2315a3a8a9..17bbb0b53c1d7e0733916bd34aaff2985aef1cec 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -186,4 +186,81 @@ describe API::V3::Users, api: true  do
       expect(response).to have_http_status(404)
     end
   end
+
+  describe 'GET /users/:id/events' do
+    let(:user) { create(:user) }
+    let(:project) { create(:empty_project) }
+    let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
+
+    before do
+      project.add_user(user, :developer)
+      EventCreateService.new.leave_note(note, user)
+    end
+
+    context "as a user than cannot see the event's project" do
+      it 'returns no events' do
+        other_user = create(:user)
+
+        get api("/users/#{user.id}/events", other_user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_empty
+      end
+    end
+
+    context "as a user than can see the event's project" do
+      context 'joined event' do
+        it 'returns the "joined" event' do
+          get v3_api("/users/#{user.id}/events", user)
+
+          expect(response).to have_http_status(200)
+          expect(response).to include_pagination_headers
+          expect(json_response).to be_an Array
+
+          comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
+
+          expect(comment_event['project_id'].to_i).to eq(project.id)
+          expect(comment_event['author_username']).to eq(user.username)
+          expect(comment_event['note']['id']).to eq(note.id)
+          expect(comment_event['note']['body']).to eq('What an awesome day!')
+
+          joined_event = json_response.find { |e| e['action_name'] == 'joined' }
+
+          expect(joined_event['project_id'].to_i).to eq(project.id)
+          expect(joined_event['author_username']).to eq(user.username)
+          expect(joined_event['author']['name']).to eq(user.name)
+        end
+      end
+
+      context 'when there are multiple events from different projects' do
+        let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+        let(:third_note) { create(:note_on_issue, project: project) }
+
+        before do
+          second_note.project.add_user(user, :developer)
+
+          [second_note, third_note].each do |note|
+            EventCreateService.new.leave_note(note, user)
+          end
+        end
+
+        it 'returns events in the correct order (from newest to oldest)' do
+          get v3_api("/users/#{user.id}/events", user)
+
+          comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+
+          expect(comment_events[0]['target_id']).to eq(third_note.id)
+          expect(comment_events[1]['target_id']).to eq(second_note.id)
+          expect(comment_events[2]['target_id']).to eq(note.id)
+        end
+      end
+    end
+
+    it 'returns a 404 error if not found' do
+      get v3_api('/users/42/events', user)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 User Not Found')
+    end
+  end
 end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 769f04c505760a3d66a36372cb826772b4509662..0c1413119e0ebe05b4b744ac2b62989ab17d5189 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -152,8 +152,9 @@ describe API::Variables, api: true do
       it 'deletes variable' do
         expect do
           delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+          expect(response).to have_http_status(204)
         end.to change{project.variables.count}.by(-1)
-        expect(response).to have_http_status(200)
       end
 
       it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index d85afdeab4244ae1dd4f78bac32162dbeb7e7f25..c879f37f50d4fac277731575044f51af68a87c78 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe Ci::API::Builds do
   include ApiHelpers
 
-  let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
+  let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
   let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
   let(:last_update) { nil }
 
@@ -81,8 +81,8 @@ describe Ci::API::Builds do
           expect(runner.reload.platform).to eq("darwin")
           expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
           expect(json_response["variables"]).to include(
-            { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-            { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+            { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+            { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
             { "key" => "DB_NAME", "value" => "postgres", "public" => true }
           )
         end
@@ -182,9 +182,9 @@ describe Ci::API::Builds do
 
           expect(response).to have_http_status(201)
           expect(json_response["variables"]).to include(
-            { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-            { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
-            { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+            { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+            { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
+            { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
             { "key" => "DB_NAME", "value" => "postgres", "public" => true },
             { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
             { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
@@ -630,6 +630,7 @@ describe Ci::API::Builds do
 
           context 'with an expire date' do
             let!(:artifacts) { file_upload }
+            let(:default_artifacts_expire_in) {}
 
             let(:post_data) do
               { 'file.path' => artifacts.path,
@@ -638,6 +639,9 @@ describe Ci::API::Builds do
             end
 
             before do
+              stub_application_setting(
+                default_artifacts_expire_in: default_artifacts_expire_in)
+
               post(post_url, post_data, headers_with_token)
             end
 
@@ -648,7 +652,8 @@ describe Ci::API::Builds do
                 build.reload
                 expect(response).to have_http_status(201)
                 expect(json_response['artifacts_expire_at']).not_to be_empty
-                expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+                expect(build.artifacts_expire_at).
+                  to be_within(5.minutes).of(7.days.from_now)
               end
             end
 
@@ -661,6 +666,32 @@ describe Ci::API::Builds do
                 expect(json_response['artifacts_expire_at']).to be_nil
                 expect(build.artifacts_expire_at).to be_nil
               end
+
+              context 'with application default' do
+                context 'default to 5 days' do
+                  let(:default_artifacts_expire_in) { '5 days' }
+
+                  it 'sets to application default' do
+                    build.reload
+                    expect(response).to have_http_status(201)
+                    expect(json_response['artifacts_expire_at'])
+                      .not_to be_empty
+                    expect(build.artifacts_expire_at)
+                      .to be_within(5.minutes).of(5.days.from_now)
+                  end
+                end
+
+                context 'default to 0' do
+                  let(:default_artifacts_expire_in) { '0' }
+
+                  it 'does not set expire_in' do
+                    build.reload
+                    expect(response).to have_http_status(201)
+                    expect(json_response['artifacts_expire_at']).to be_nil
+                    expect(build.artifacts_expire_at).to be_nil
+                  end
+                end
+              end
             end
           end
 
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index bd55934d0c8f9563559b294c1ce51018871c989a..d50cdfdc2d6885c46a489106f21409789a112b01 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -18,6 +18,7 @@ describe Ci::API::Runners do
       it 'creates runner with default values' do
         expect(response).to have_http_status 201
         expect(Ci::Runner.first.run_untagged).to be true
+        expect(Ci::Runner.first.token).not_to eq(registration_token)
       end
     end
 
@@ -41,7 +42,7 @@ describe Ci::API::Runners do
 
       it 'creates runner' do
         expect(response).to have_http_status 201
-        expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
+        expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
       end
     end
 
@@ -74,6 +75,8 @@ describe Ci::API::Runners do
       it 'creates runner' do
         expect(response).to have_http_status 201
         expect(project.runners.size).to eq(1)
+        expect(Ci::Runner.first.token).not_to eq(registration_token)
+        expect(Ci::Runner.first.token).not_to eq(project.runners_token)
       end
     end
 
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 87786e8562113de7f91009d1a39bcae59cf5bdd4..006d6a6af1c28c640434c2374ba33a532fbba759 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do
               end
 
               context "when the user is blocked" do
-                it "responds with status 404" do
+                it "responds with status 401" do
                   user.block
                   project.team << [user, :master]
 
                   download(path, env) do |response|
-                    expect(response).to have_http_status(404)
+                    expect(response).to have_http_status(401)
+                  end
+                end
+
+                it "responds with status 401 for unknown projects (no project existence information leak)" do
+                  user.block
+
+                  download('doesnt/exist.git', env) do |response|
+                    expect(response).to have_http_status(401)
                   end
                 end
               end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index c0e7bab8199207f1a506a095d2505d1b05d08438..5d495bc9e7dd27fe9cd0aeba3dc61b4ac2dfefbf 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -25,11 +25,9 @@ describe 'Git LFS API and storage' do
       {
         'objects' => [
           { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-            'size' => 1575078
-          },
+            'size' => 1575078 },
           { 'oid' => sample_oid,
-            'size' => sample_size
-          }
+            'size' => sample_size }
         ],
         'operation' => 'upload'
       }
@@ -53,11 +51,9 @@ describe 'Git LFS API and storage' do
       {
         'objects' => [
           { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-            'size' => 1575078
-          },
+            'size' => 1575078 },
           { 'oid' => sample_oid,
-            'size' => sample_size
-          }
+            'size' => sample_size }
         ],
         'operation' => 'upload'
       }
@@ -374,11 +370,12 @@ describe 'Git LFS API and storage' do
     describe 'download' do
       let(:project) { create(:empty_project) }
       let(:body) do
-        { 'operation' => 'download',
+        {
+          'operation' => 'download',
           'objects' => [
             { 'oid' => sample_oid,
-              'size' => sample_size
-            }]
+              'size' => sample_size }
+          ]
         }
       end
 
@@ -393,16 +390,20 @@ describe 'Git LFS API and storage' do
           end
 
           it 'with href to download' do
-            expect(json_response).to eq('objects' => [
-              { 'oid' => sample_oid,
-                'size' => sample_size,
-                'actions' => {
-                  'download' => {
-                    'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
-                    'header' => { 'Authorization' => authorization }
+            expect(json_response).to eq({
+              'objects' => [
+                {
+                  'oid' => sample_oid,
+                  'size' => sample_size,
+                  'actions' => {
+                    'download' => {
+                      'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+                      'header' => { 'Authorization' => authorization }
+                    }
                   }
                 }
-              }])
+              ]
+            })
           end
         end
 
@@ -417,24 +418,29 @@ describe 'Git LFS API and storage' do
           end
 
           it 'with href to download' do
-            expect(json_response).to eq('objects' => [
-              { 'oid' => sample_oid,
-                'size' => sample_size,
-                'error' => {
-                  'code' => 404,
-                  'message' => "Object does not exist on the server or you don't have permissions to access it",
+            expect(json_response).to eq({
+              'objects' => [
+                {
+                  'oid' => sample_oid,
+                  'size' => sample_size,
+                  'error' => {
+                    'code' => 404,
+                    'message' => "Object does not exist on the server or you don't have permissions to access it",
+                  }
                 }
-              }])
+              ]
+            })
           end
         end
 
         context 'when downloading a lfs object that does not exist' do
           let(:body) do
-            { 'operation' => 'download',
+            {
+              'operation' => 'download',
               'objects' => [
                 { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                  'size' => 1575078
-                }]
+                  'size' => 1575078 }
+              ]
             }
           end
 
@@ -443,27 +449,30 @@ describe 'Git LFS API and storage' do
           end
 
           it 'with an 404 for specific object' do
-            expect(json_response).to eq('objects' => [
-              { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                'size' => 1575078,
-                'error' => {
-                  'code' => 404,
-                  'message' => "Object does not exist on the server or you don't have permissions to access it",
+            expect(json_response).to eq({
+              'objects' => [
+                {
+                  'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+                  'size' => 1575078,
+                  'error' => {
+                    'code' => 404,
+                    'message' => "Object does not exist on the server or you don't have permissions to access it",
+                  }
                 }
-              }])
+              ]
+            })
           end
         end
 
         context 'when downloading one new and one existing lfs object' do
           let(:body) do
-            { 'operation' => 'download',
+            {
+              'operation' => 'download',
               'objects' => [
                 { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                  'size' => 1575078
-                },
+                  'size' => 1575078 },
                 { 'oid' => sample_oid,
-                  'size' => sample_size
-                }
+                  'size' => sample_size }
               ]
             }
           end
@@ -477,23 +486,28 @@ describe 'Git LFS API and storage' do
           end
 
           it 'responds with upload hypermedia link for the new object' do
-            expect(json_response).to eq('objects' => [
-              { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                'size' => 1575078,
-                'error' => {
-                  'code' => 404,
-                  'message' => "Object does not exist on the server or you don't have permissions to access it",
-                }
-              },
-              { 'oid' => sample_oid,
-                'size' => sample_size,
-                'actions' => {
-                  'download' => {
-                    'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
-                    'header' => { 'Authorization' => authorization }
+            expect(json_response).to eq({
+              'objects' => [
+                {
+                  'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+                  'size' => 1575078,
+                  'error' => {
+                    'code' => 404,
+                    'message' => "Object does not exist on the server or you don't have permissions to access it",
+                  }
+                },
+                {
+                  'oid' => sample_oid,
+                  'size' => sample_size,
+                  'actions' => {
+                    'download' => {
+                      'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+                      'header' => { 'Authorization' => authorization }
+                    }
                   }
                 }
-              }])
+              ]
+            })
           end
         end
       end
@@ -597,17 +611,21 @@ describe 'Git LFS API and storage' do
           end
 
           it 'responds with status 200 and href to download' do
-            expect(json_response).to eq('objects' => [
-              { 'oid' => sample_oid,
-                'size' => sample_size,
-                'authenticated' => true,
-                'actions' => {
-                  'download' => {
-                    'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
-                    'header' => {}
+            expect(json_response).to eq({
+              'objects' => [
+                {
+                  'oid' => sample_oid,
+                  'size' => sample_size,
+                  'authenticated' => true,
+                  'actions' => {
+                    'download' => {
+                      'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+                      'header' => {}
+                    }
                   }
                 }
-              }])
+              ]
+            })
           end
         end
 
@@ -626,11 +644,12 @@ describe 'Git LFS API and storage' do
     describe 'upload' do
       let(:project) { create(:project, :public) }
       let(:body) do
-        { 'operation' => 'upload',
+        {
+          'operation' => 'upload',
           'objects' => [
             { 'oid' => sample_oid,
-              'size' => sample_size
-            }]
+              'size' => sample_size }
+          ]
         }
       end
 
@@ -665,11 +684,12 @@ describe 'Git LFS API and storage' do
 
           context 'when pushing a lfs object that does not exist' do
             let(:body) do
-              { 'operation' => 'upload',
+              {
+                'operation' => 'upload',
                 'objects' => [
                   { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                    'size' => 1575078
-                  }]
+                    'size' => 1575078 }
+                ]
               }
             end
 
@@ -688,14 +708,13 @@ describe 'Git LFS API and storage' do
 
           context 'when pushing one new and one existing lfs object' do
             let(:body) do
-              { 'operation' => 'upload',
+              {
+                'operation' => 'upload',
                 'objects' => [
                   { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
-                    'size' => 1575078
-                  },
+                    'size' => 1575078 },
                   { 'oid' => sample_oid,
-                    'size' => sample_size
-                  }
+                    'size' => sample_size }
                 ]
               }
             end
@@ -789,11 +808,12 @@ describe 'Git LFS API and storage' do
       let(:project) { create(:empty_project) }
       let(:authorization) { authorize_user }
       let(:body) do
-        { 'operation' => 'other',
+        {
+          'operation' => 'other',
           'objects' => [
             { 'oid' => sample_oid,
-              'size' => sample_size
-            }]
+              'size' => sample_size }
+          ]
         }
       end
 
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5206634bca52d42c419774ae89721515332d9c41
--- /dev/null
+++ b/spec/requests/openid_connect_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe 'OpenID Connect requests' do
+  include ApiHelpers
+
+  let(:user) { create :user }
+  let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
+  let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
+
+  def request_access_token
+    login_as user
+
+    post '/oauth/token',
+      grant_type: 'authorization_code',
+      code: access_grant.token,
+      redirect_uri: application.redirect_uri,
+      client_id: application.uid,
+      client_secret: application.secret
+  end
+
+  def request_user_info
+    get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
+  end
+
+  def hashed_subject
+    Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
+  end
+
+  context 'Application without OpenID scope' do
+    let(:application) { create :oauth_application, scopes: 'api' }
+
+    it 'token response does not include an ID token' do
+      request_access_token
+
+      expect(json_response).to include 'access_token'
+      expect(json_response).not_to include 'id_token'
+    end
+
+    it 'userinfo response is unauthorized' do
+      request_user_info
+
+      expect(response).to have_http_status 403
+      expect(response.body).to be_blank
+    end
+  end
+
+  context 'Application with OpenID scope' do
+    let(:application) { create :oauth_application, scopes: 'openid' }
+
+    it 'token response includes an ID token' do
+      request_access_token
+
+      expect(json_response).to include 'id_token'
+    end
+
+    context 'UserInfo payload' do
+      let(:user) do
+        create(
+          :user,
+          name: 'Alice',
+          username: 'alice',
+          emails: [private_email, public_email],
+          email: private_email.email,
+          public_email: public_email.email,
+          website_url: 'https://example.com',
+          avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
+        )
+      end
+
+      let(:public_email) { build :email, email: 'public@example.com' }
+      let(:private_email) { build :email, email: 'private@example.com' }
+
+      it 'includes all user information' do
+        request_user_info
+
+        expect(json_response).to eq({
+          'sub'            => hashed_subject,
+          'name'           => 'Alice',
+          'nickname'       => 'alice',
+          'email'          => 'public@example.com',
+          'email_verified' => true,
+          'website'        => 'https://example.com',
+          'profile'        => 'http://localhost/alice',
+          'picture'        => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
+        })
+      end
+    end
+
+    context 'ID token payload' do
+      before do
+        request_access_token
+        @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
+      end
+
+      it 'includes the Gitlab root URL' do
+        expect(@payload['iss']).to eq Gitlab.config.gitlab.url
+      end
+
+      it 'includes the hashed user ID' do
+        expect(@payload['sub']).to eq hashed_subject
+      end
+
+      it 'includes the time of the last authentication' do
+        expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i
+      end
+
+      it 'does not include any unknown properties' do
+        expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
+      end
+    end
+
+    context 'when user is blocked' do
+      it 'returns authentication error' do
+        access_grant
+        user.block
+
+        expect do
+          request_access_token
+        end.to throw_symbol :warden
+      end
+    end
+
+    context 'when user is ldap_blocked' do
+      it 'returns authentication error' do
+        access_grant
+        user.ldap_block
+
+        expect do
+          request_access_token
+        end.to throw_symbol :warden
+      end
+    end
+  end
+end
diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c3bc08f1a1c025c7b3987430840ea6aef4656b5
--- /dev/null
+++ b/spec/routing/openid_connect_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+# oauth_discovery_keys      GET /oauth/discovery/keys(.:format)             doorkeeper/openid_connect/discovery#keys
+# oauth_discovery_provider  GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
+# oauth_discovery_webfinger GET /.well-known/webfinger(.:format)            doorkeeper/openid_connect/discovery#webfinger
+describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
+  it "to #provider" do
+    expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider')
+  end
+
+  it "to #webfinger" do
+    expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger')
+  end
+
+  it "to #keys" do
+    expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
+  end
+end
+
+# oauth_userinfo GET  /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+#                POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do
+  it "to #show" do
+    expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+  end
+
+  it "to #show" do
+    expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+  end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a5bc62ef6c2b4a6bc554d8e8b051610dac8f914d..4baccacd448e310ee570c1bec34303a350706b0c 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -120,7 +120,6 @@ 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
@@ -128,7 +127,7 @@ describe 'project routing' do
   # 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|
+    [: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
@@ -431,12 +430,22 @@ describe 'project routing' do
     end
   end
 
-  #         project_notes GET    /:project_id/notes(.:format)         notes#index
-  #                       POST   /:project_id/notes(.:format)         notes#create
-  #          project_note DELETE /:project_id/notes/:id(.:format)     notes#destroy
+  # project_noteable_notes GET    /:project_id/noteable/:target_type/:target_id/notes notes#index
+  #                        POST   /:project_id/notes(.:format)                        notes#create
+  #           project_note DELETE /:project_id/notes/:id(.:format)                    notes#destroy
   describe Projects::NotesController, 'routing' do
+    it 'to #index' do
+      expect(get('/gitlab/gitlabhq/noteable/issue/1/notes')).to route_to(
+        'projects/notes#index',
+        namespace_id: 'gitlab',
+        project_id: 'gitlabhq',
+        target_type: 'issue',
+        target_id: '1'
+      )
+    end
+
     it_behaves_like 'RESTful project resources' do
-      let(:actions)    { [:index, :create, :destroy] }
+      let(:actions)    { [:create, :destroy] }
       let(:controller) { 'notes' }
     end
   end
diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..381d7871a40c13259db603b6f3667c1286ea9851
--- /dev/null
+++ b/spec/rubocop/cop/custom_error_class_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/custom_error_class'
+
+describe RuboCop::Cop::CustomErrorClass do
+  include CopHelper
+
+  subject(:cop) { described_class.new }
+
+  context 'when a class has a body' do
+    it 'does nothing' do
+      inspect_source(cop, 'class CustomError < StandardError; def foo; end; end')
+
+      expect(cop.offenses).to be_empty
+    end
+  end
+
+  context 'when a class has no explicit superclass' do
+    it 'does nothing' do
+      inspect_source(cop, 'class CustomError; end')
+
+      expect(cop.offenses).to be_empty
+    end
+  end
+
+  context 'when a class has a superclass that does not end in Error' do
+    it 'does nothing' do
+      inspect_source(cop, 'class CustomError < BasicObject; end')
+
+      expect(cop.offenses).to be_empty
+    end
+  end
+
+  context 'when a class is empty and inherits from a class ending in Error' do
+    context 'when the class is on a single line' do
+      let(:source) do
+        <<-SOURCE
+          module Foo
+            class CustomError < Bar::Baz::BaseError; end
+          end
+        SOURCE
+      end
+
+      let(:expected) do
+        <<-EXPECTED
+          module Foo
+            CustomError = Class.new(Bar::Baz::BaseError)
+          end
+        EXPECTED
+      end
+
+      it 'registers an offense' do
+        expected_highlights = source.split("\n")[1].strip
+
+        inspect_source(cop, source)
+
+        aggregate_failures do
+          expect(cop.offenses.size).to eq(1)
+          expect(cop.offenses.map(&:line)).to eq([2])
+          expect(cop.highlights).to contain_exactly(expected_highlights)
+        end
+      end
+
+      it 'autocorrects to the right version' do
+        autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
+
+        expect(autocorrected).to eq(expected)
+      end
+    end
+
+    context 'when the class is on multiple lines' do
+      let(:source) do
+        <<-SOURCE
+          module Foo
+            class CustomError < Bar::Baz::BaseError
+            end
+          end
+        SOURCE
+      end
+
+      let(:expected) do
+        <<-EXPECTED
+          module Foo
+            CustomError = Class.new(Bar::Baz::BaseError)
+          end
+        EXPECTED
+      end
+
+      it 'registers an offense' do
+        expected_highlights = source.split("\n")[1..2].join("\n").strip
+
+        inspect_source(cop, source)
+
+        aggregate_failures do
+          expect(cop.offenses.size).to eq(1)
+          expect(cop.offenses.map(&:line)).to eq([2])
+          expect(cop.highlights).to contain_exactly(expected_highlights)
+        end
+      end
+
+      it 'autocorrects to the right version' do
+        autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
+
+        expect(autocorrected).to eq(expected)
+      end
+    end
+  end
+end
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..19a5718b0b16ae797f56060c87a5efc3797a1d75
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_concurrent_index'
+
+describe RuboCop::Cop::Migration::AddConcurrentIndex do
+  include CopHelper
+
+  subject(:cop) { described_class.new }
+
+  context 'in migration' do
+    before do
+      allow(cop).to receive(:in_migration?).and_return(true)
+    end
+
+    it 'registers an offense when add_concurrent_index is used inside a change method' do
+      inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+      aggregate_failures do
+        expect(cop.offenses.size).to eq(1)
+        expect(cop.offenses.map(&:line)).to eq([1])
+      end
+    end
+
+    it 'registers no offense when add_concurrent_index is used inside an up method' do
+      inspect_source(cop, 'def up; add_concurrent_index :table, :column; end')
+
+      expect(cop.offenses.size).to eq(0)
+    end
+  end
+
+  context 'outside of migration' do
+    it 'registers no offense' do
+      inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+      expect(cop.offenses.size).to eq(0)
+    end
+  end
+end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index bb26513103d08b0efeddeeec95fdd804cc6788a0..b91234ddb1e71dd303a89657e8b7b200567e1f95 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -72,7 +72,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
 
   shared_examples 'a pullable and pushable' do
     it_behaves_like 'a accessible' do
-      let(:actions) { ['pull', 'push'] }
+      let(:actions) { %w(pull push) }
     end
   end
 
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 305278843f54c82ee7cb9296de00b98ba9f2d569..22115c6566d79dfd9edbffc1cc7ccd547a5af95d 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -43,8 +43,8 @@ describe Boards::Issues::ListService, services: true do
       described_class.new(project, user, params).execute
     end
 
-    context 'sets default order to priority' do
-      it 'returns opened issues when list id is missing' do
+    context 'issues are ordered by priority' do
+      it 'returns opened issues when list_id is missing' do
         params = { board_id: board.id }
 
         issues = described_class.new(project, user, params).execute
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 77f75167b3d8dd806a1978f03f525c7ccd915c69..727ea04ea5ce47db2d6940a2c963e66fc0ce22ae 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -78,8 +78,10 @@ describe Boards::Issues::MoveService, services: true do
     end
 
     context 'when moving to same list' do
-      let(:issue)  { create(:labeled_issue, project: project, labels: [bug, development]) }
-      let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
+      let(:issue)   { create(:labeled_issue, project: project, labels: [bug, development]) }
+      let(:issue1)  { create(:labeled_issue, project: project, labels: [bug, development]) }
+      let(:issue2)  { create(:labeled_issue, project: project, labels: [bug, development]) }
+      let(:params)  { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
 
       it 'returns false' do
         expect(described_class.new(project, user, params).execute(issue)).to eq false
@@ -90,6 +92,18 @@ describe Boards::Issues::MoveService, services: true do
 
         expect(issue.reload.labels).to contain_exactly(bug, development)
       end
+
+      it 'sorts issues' do
+        [issue, issue1, issue2].each do |issue|
+          issue.move_to_end && issue.save!
+        end
+
+        params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
+
+        described_class.new(project, user, params).execute(issue)
+
+        expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+      end
     end
   end
 end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ceaca96e25b377a1ace0e89bef1de526261ea9d1..a969829a63e108bd5462960f91f3f1574a26bb77 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -79,66 +79,53 @@ describe Ci::CreatePipelineService, services: true do
 
     context 'when commit contains a [ci skip] directive' do
       let(:message) { "some message[ci skip]" }
-      let(:messageFlip) { "some message[skip ci]" }
-      let(:capMessage) { "some message[CI SKIP]" }
-      let(:capMessageFlip) { "some message[SKIP CI]" }
+
+      ci_messages = [
+        "some message[ci skip]",
+        "some message[skip ci]",
+        "some message[CI SKIP]",
+        "some message[SKIP CI]",
+        "some message[ci_skip]",
+        "some message[skip_ci]",
+        "some message[ci-skip]",
+        "some message[skip-ci]"
+      ]
 
       before do
         allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
       end
 
-      it "skips builds creation if there is [ci skip] tag in commit message" do
-        commits = [{ message: message }]
-        pipeline = execute(ref: 'refs/heads/master',
-                           before: '00000000',
-                           after: project.commit.id,
-                           commits: commits)
+      ci_messages.each do |ci_message|
+        it "skips builds creation if the commit message is #{ci_message}" do
+          commits = [{ message: ci_message }]
+          pipeline = execute(ref: 'refs/heads/master',
+                             before: '00000000',
+                             after: project.commit.id,
+                             commits: commits)
 
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [skip ci] tag in commit message" do
-        commits = [{ message: messageFlip }]
-        pipeline = execute(ref: 'refs/heads/master',
-                           before: '00000000',
-                           after: project.commit.id,
-                           commits: commits)
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
+          expect(pipeline).to be_persisted
+          expect(pipeline.builds.any?).to be false
+          expect(pipeline.status).to eq("skipped")
+        end
       end
 
-      it "skips builds creation if there is [CI SKIP] tag in commit message" do
-        commits = [{ message: capMessage }]
-        pipeline = execute(ref: 'refs/heads/master',
-                           before: '00000000',
-                           after: project.commit.id,
-                           commits: commits)
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
+      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
 
-      it "skips builds creation if there is [SKIP CI] tag in commit message" do
-        commits = [{ message: capMessageFlip }]
+        commits = [{ message: "some message" }]
         pipeline = execute(ref: 'refs/heads/master',
                            before: '00000000',
                            after: project.commit.id,
                            commits: commits)
 
         expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
+        expect(pipeline.builds.first.name).to eq("rspec")
       end
 
-      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
-        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+      it "does not skip builds creation if the commit message is nil" do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil }
 
-        commits = [{ message: "some message" }]
+        commits = [{ message: nil }]
         pipeline = execute(ref: 'refs/heads/master',
                            before: '00000000',
                            after: project.commit.id,
@@ -213,7 +200,7 @@ describe Ci::CreatePipelineService, services: true do
 
     context 'with environment' do
       before do
-        config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+        config = YAML.dump(deploy: { environment: { name: "review/$CI_COMMIT_REF_NAME" }, script: 'ls' })
         stub_ci_pipeline_yaml_file(config)
       end
 
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index d8c443d29d5646d8ee788f8aad5f6f55262d89d5..5e68343784d8b08bbb3ff99c658a8903c4236cf8 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -13,8 +13,22 @@ describe Ci::CreateTriggerRequestService, services: true do
     context 'valid params' do
       subject { service.execute(project, trigger, 'master') }
 
-      it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
-      it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+      context 'without owner' do
+        it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+        it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+        it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+      end
+
+      context 'with owner' do
+        let(:owner) { create(:user) }
+        let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+
+        it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+        it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+        it { expect(subject.pipeline.user).to eq(owner) }
+        it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+        it { expect(subject.builds.first.user).to eq(owner) }
+      end
     end
 
     context 'no commit for ref' do
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
deleted file mode 100644
index b3e0a7b9b58b19fd4eee1f222083b6e5d37f5a93..0000000000000000000000000000000000000000
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-module Ci
-  describe ImageForBuildService, services: true do
-    let(:service) { ImageForBuildService.new }
-    let(:project) { FactoryGirl.create(:empty_project) }
-    let(:commit_sha) { '01234567890123456789' }
-    let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
-    let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
-
-    describe '#execute' do
-      before { build }
-
-      context 'branch name' do
-        before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) }
-        before { build.run! }
-        let(:image) { service.execute(project, ref: 'master') }
-
-        it { expect(image).to be_kind_of(OpenStruct) }
-        it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
-        it { expect(image.name).to eq('build-running.svg') }
-      end
-
-      context 'unknown branch name' do
-        let(:image) { service.execute(project, ref: 'feature') }
-
-        it { expect(image).to be_kind_of(OpenStruct) }
-        it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
-        it { expect(image.name).to eq('build-unknown.svg') }
-      end
-
-      context 'commit sha' do
-        before { build.run! }
-        let(:image) { service.execute(project, sha: build.sha) }
-
-        it { expect(image).to be_kind_of(OpenStruct) }
-        it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
-        it { expect(image.name).to eq('build-running.svg') }
-      end
-
-      context 'unknown commit sha' do
-        let(:image) { service.execute(project, sha: '0000000') }
-
-        it { expect(image).to be_kind_of(OpenStruct) }
-        it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
-        it { expect(image.name).to eq('build-unknown.svg') }
-      end
-    end
-  end
-end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ef2ddc4b1d782391542eb16551b6baecbe9df2f9..d93616c4f50d26db7d5171cae94c459707ecdfac 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Ci::ProcessPipelineService, :services do
+describe Ci::ProcessPipelineService, '#execute', :services do
   let(:user) { create(:user) }
   let(:project) { create(:empty_project) }
 
@@ -12,381 +12,518 @@ describe Ci::ProcessPipelineService, :services do
     project.add_developer(user)
   end
 
-  describe '#execute' do
-    context 'start queuing next builds' do
-      before do
-        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
-        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
-        create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
-        create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
-        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
-      end
+  context 'when simple pipeline is defined' do
+    before do
+      create_build('linux', stage_idx: 0)
+      create_build('mac', stage_idx: 0)
+      create_build('rspec', stage_idx: 1)
+      create_build('rubocop', stage_idx: 1)
+      create_build('deploy', stage_idx: 2)
+    end
 
-      it 'processes a pipeline' do
-        expect(process_pipeline).to be_truthy
-        succeed_pending
-        expect(builds.success.count).to eq(2)
+    it 'processes a pipeline' do
+      expect(process_pipeline).to be_truthy
 
-        expect(process_pipeline).to be_truthy
-        succeed_pending
-        expect(builds.success.count).to eq(4)
+      succeed_pending
+
+      expect(builds.success.count).to eq(2)
+      expect(process_pipeline).to be_truthy
+
+      succeed_pending
+
+      expect(builds.success.count).to eq(4)
+      expect(process_pipeline).to be_truthy
+
+      succeed_pending
+
+      expect(builds.success.count).to eq(5)
+      expect(process_pipeline).to be_falsey
+    end
+
+    it 'does not process pipeline if existing stage is running' do
+      expect(process_pipeline).to be_truthy
+      expect(builds.pending.count).to eq(2)
+
+      expect(process_pipeline).to be_falsey
+      expect(builds.pending.count).to eq(2)
+    end
+  end
+
+  context 'custom stage with first job allowed to fail' do
+    before do
+      create_build('clean_job', stage_idx: 0, allow_failure: true)
+      create_build('test_job', stage_idx: 1, allow_failure: true)
+    end
 
+    it 'automatically triggers a next stage when build finishes' do
+      expect(process_pipeline).to be_truthy
+      expect(builds_statuses).to eq ['pending']
+
+      fail_running_or_pending
+
+      expect(builds_statuses).to eq %w(failed pending)
+    end
+  end
+
+  context 'when optional manual actions are defined' do
+    before do
+      create_build('build', stage_idx: 0)
+      create_build('test', stage_idx: 1)
+      create_build('test_failure', stage_idx: 2, when: 'on_failure')
+      create_build('deploy', stage_idx: 3)
+      create_build('production', stage_idx: 3, when: 'manual', allow_failure: true)
+      create_build('cleanup', stage_idx: 4, when: 'always')
+      create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true)
+    end
+
+    context 'when builds are successful' do
+      it 'properly processes the pipeline' do
         expect(process_pipeline).to be_truthy
-        succeed_pending
-        expect(builds.success.count).to eq(5)
+        expect(builds_names).to eq ['build']
+        expect(builds_statuses).to eq ['pending']
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test)
+        expect(builds_statuses).to eq %w(success pending)
 
-        expect(process_pipeline).to be_falsey
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test deploy production)
+        expect(builds_statuses).to eq %w(success success pending manual)
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test deploy production cleanup clear:cache)
+        expect(builds_statuses).to eq %w(success success success manual pending manual)
+
+        succeed_running_or_pending
+
+        expect(builds_statuses).to eq %w(success success success manual success manual)
+        expect(pipeline.reload.status).to eq 'success'
       end
+    end
 
-      it 'does not process pipeline if existing stage is running' do
+    context 'when test job fails' do
+      it 'properly processes the pipeline' do
         expect(process_pipeline).to be_truthy
-        expect(builds.pending.count).to eq(2)
+        expect(builds_names).to eq ['build']
+        expect(builds_statuses).to eq ['pending']
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test)
+        expect(builds_statuses).to eq %w(success pending)
 
-        expect(process_pipeline).to be_falsey
-        expect(builds.pending.count).to eq(2)
+        fail_running_or_pending
+
+        expect(builds_names).to eq %w(build test test_failure)
+        expect(builds_statuses).to eq %w(success failed pending)
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test test_failure cleanup)
+        expect(builds_statuses).to eq %w(success failed success pending)
+
+        succeed_running_or_pending
+
+        expect(builds_statuses).to eq %w(success failed success success)
+        expect(pipeline.reload.status).to eq 'failed'
       end
     end
 
-    context 'custom stage with first job allowed to fail' do
-      before do
-        create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
-        create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+    context 'when test and test_failure jobs fail' do
+      it 'properly processes the pipeline' do
+        expect(process_pipeline).to be_truthy
+        expect(builds_names).to eq ['build']
+        expect(builds_statuses).to eq ['pending']
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test)
+        expect(builds_statuses).to eq %w(success pending)
+
+        fail_running_or_pending
+
+        expect(builds_names).to eq %w(build test test_failure)
+        expect(builds_statuses).to eq %w(success failed pending)
+
+        fail_running_or_pending
+
+        expect(builds_names).to eq %w(build test test_failure cleanup)
+        expect(builds_statuses).to eq %w(success failed failed pending)
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test test_failure cleanup)
+        expect(builds_statuses).to eq %w(success failed failed success)
+        expect(pipeline.reload.status).to eq('failed')
       end
+    end
 
-      it 'automatically triggers a next stage when build finishes' do
+    context 'when deploy job fails' do
+      it 'properly processes the pipeline' do
         expect(process_pipeline).to be_truthy
-        expect(builds.pluck(:status)).to contain_exactly('pending')
+        expect(builds_names).to eq ['build']
+        expect(builds_statuses).to eq ['pending']
+
+        succeed_running_or_pending
 
-        pipeline.builds.running_or_pending.each(&:drop)
-        expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+        expect(builds_names).to eq %w(build test)
+        expect(builds_statuses).to eq %w(success pending)
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w(build test deploy production)
+        expect(builds_statuses).to eq %w(success success pending manual)
+
+        fail_running_or_pending
+
+        expect(builds_names).to eq %w(build test deploy production cleanup)
+        expect(builds_statuses).to eq %w(success success failed manual pending)
+
+        succeed_running_or_pending
+
+        expect(builds_statuses).to eq %w(success success failed manual success)
+        expect(pipeline.reload).to be_failed
       end
     end
 
-    context 'properly creates builds when "when" is defined' do
-      before do
-        create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
-        create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
-        create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
-        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
-        create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
-        create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
-        create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
-      end
+    context 'when build is canceled in the second stage' do
+      it 'does not schedule builds after build has been canceled' do
+        expect(process_pipeline).to be_truthy
+        expect(builds_names).to eq ['build']
+        expect(builds_statuses).to eq ['pending']
 
-      context 'when builds are successful' do
-        it 'properly creates builds' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build')
-          expect(builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('success')
-        end
-      end
+        succeed_running_or_pending
 
-      context 'when test job fails' do
-        it 'properly creates builds' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build')
-          expect(builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
+        expect(builds.running_or_pending).not_to be_empty
+        expect(builds_names).to eq %w(build test)
+        expect(builds_statuses).to eq %w(success pending)
 
-      context 'when test and test_failure jobs fail' do
-        it 'properly creates builds' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build')
-          expect(builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
+        cancel_running_or_pending
 
-      context 'when deploy job fails' do
-        it 'properly creates builds' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build')
-          expect(builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
+        expect(builds.running_or_pending).to be_empty
+        expect(builds_names).to eq %w[build test]
+        expect(builds_statuses).to eq %w[success canceled]
+        expect(pipeline.reload).to be_canceled
       end
+    end
 
-      context 'when build is canceled in the second stage' do
-        it 'does not schedule builds after build has been canceled' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build')
-          expect(builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
+    context 'when listing optional manual actions' do
+      it 'returns only for skipped builds' do
+        # currently all builds are created
+        expect(process_pipeline).to be_truthy
+        expect(manual_actions).to be_empty
 
-          expect(builds.running_or_pending).not_to be_empty
+        # succeed stage build
+        succeed_running_or_pending
 
-          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:cancel)
+        expect(manual_actions).to be_empty
 
-          expect(builds.running_or_pending).to be_empty
-          expect(pipeline.reload.status).to eq('canceled')
-        end
-      end
+        # succeed stage test
+        succeed_running_or_pending
 
-      context 'when listing manual actions' do
-        it 'returns only for skipped builds' do
-          # currently all builds are created
-          expect(process_pipeline).to be_truthy
-          expect(manual_actions).to be_empty
+        expect(manual_actions).to be_one # production
 
-          # succeed stage build
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_empty
+        # succeed stage deploy
+        succeed_running_or_pending
 
-          # succeed stage test
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_one # production
+        expect(manual_actions).to be_many # production and clear cache
+      end
+    end
+  end
+
+  context 'when there are manual action in earlier stages' do
+    context 'when first stage has only optional manual actions' do
+      before do
+        create_build('build', stage_idx: 0, when: 'manual', allow_failure: true)
+        create_build('check', stage_idx: 1)
+        create_build('test', stage_idx: 2)
 
-          # succeed stage deploy
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_many # production and clear cache
-        end
+        process_pipeline
+      end
+
+      it 'starts from the second stage' do
+        expect(all_builds_statuses).to eq %w[manual pending created]
       end
     end
 
-    context 'when there are manual/on_failure jobs in earlier stages' do
+    context 'when second stage has only optional manual actions' do
       before do
-        builds
+        create_build('check', stage_idx: 0)
+        create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
+        create_build('test', stage_idx: 2)
+
         process_pipeline
-        builds.each(&:reload)
       end
 
-      context 'when first stage has only manual jobs' do
-        let(:builds) do
-          [create_build('build', 0, 'manual'),
-           create_build('check', 1),
-           create_build('test', 2)]
-        end
+      it 'skips second stage and continues on third stage' do
+        expect(all_builds_statuses).to eq(%w[pending created created])
 
-        it 'starts from the second stage' do
-          expect(builds.map(&:status)).to eq(%w[skipped pending created])
-        end
+        builds.first.success
+
+        expect(all_builds_statuses).to eq(%w[success manual pending])
       end
+    end
+  end
+
+  context 'when blocking manual actions are defined' do
+    before do
+      create_build('code:test', stage_idx: 0)
+      create_build('staging:deploy', stage_idx: 1, when: 'manual')
+      create_build('staging:test', stage_idx: 2, when: 'on_success')
+      create_build('production:deploy', stage_idx: 3, when: 'manual')
+      create_build('production:test', stage_idx: 4, when: 'always')
+    end
 
-      context 'when second stage has only manual jobs' do
-        let(:builds) do
-          [create_build('check', 0),
-           create_build('build', 1, 'manual'),
-           create_build('test', 2)]
-        end
+    context 'when first stage succeeds' do
+      it 'blocks pipeline on stage with first manual action' do
+        process_pipeline
 
-        it 'skips second stage and continues on third stage' do
-          expect(builds.map(&:status)).to eq(%w[pending created created])
+        expect(builds_names).to eq %w[code:test]
+        expect(builds_statuses).to eq %w[pending]
+        expect(pipeline.reload.status).to eq 'pending'
 
-          builds.first.success
-          builds.each(&:reload)
+        succeed_running_or_pending
 
-          expect(builds.map(&:status)).to eq(%w[success skipped pending])
-        end
+        expect(builds_names).to eq %w[code:test staging:deploy]
+        expect(builds_statuses).to eq %w[success manual]
+        expect(pipeline.reload).to be_manual
       end
+    end
+
+    context 'when first stage fails' do
+      it 'does not take blocking action into account' do
+        process_pipeline
+
+        expect(builds_names).to eq %w[code:test]
+        expect(builds_statuses).to eq %w[pending]
+        expect(pipeline.reload.status).to eq 'pending'
 
-      context 'when second stage has only on_failure jobs' do
-        let(:builds) do
-          [create_build('check', 0),
-           create_build('build', 1, 'on_failure'),
-           create_build('test', 2)]
-        end
+        fail_running_or_pending
 
-        it 'skips second stage and continues on third stage' do
-          expect(builds.map(&:status)).to eq(%w[pending created created])
+        expect(builds_names).to eq %w[code:test production:test]
+        expect(builds_statuses).to eq %w[failed pending]
 
-          builds.first.success
-          builds.each(&:reload)
+        succeed_running_or_pending
 
-          expect(builds.map(&:status)).to eq(%w[success skipped pending])
-        end
+        expect(builds_statuses).to eq %w[failed success]
+        expect(pipeline.reload).to be_failed
       end
     end
 
-    context 'when failed build in the middle stage is retried' do
-      context 'when failed build is the only unsuccessful build in the stage' do
-        before do
-          create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
-          create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
-          create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
-          create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
-          create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
-          create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
-        end
+    context 'when pipeline is promoted sequentially up to the end' do
+      it 'properly processes entire pipeline' do
+        process_pipeline
+
+        expect(builds_names).to eq %w[code:test]
+        expect(builds_statuses).to eq %w[pending]
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w[code:test staging:deploy]
+        expect(builds_statuses).to eq %w[success manual]
+        expect(pipeline.reload).to be_manual
+
+        play_manual_action('staging:deploy')
+
+        expect(builds_statuses).to eq %w[success pending]
+
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w[code:test staging:deploy staging:test]
+        expect(builds_statuses).to eq %w[success success pending]
+
+        succeed_running_or_pending
 
-        it 'does trigger builds in the next stage' do
-          expect(process_pipeline).to be_truthy
-          expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+        expect(builds_names).to eq %w[code:test staging:deploy staging:test
+                                      production:deploy]
+        expect(builds_statuses).to eq %w[success success success manual]
 
-          pipeline.builds.running_or_pending.each(&:success)
+        expect(pipeline.reload).to be_manual
+        expect(pipeline.reload).to be_blocked
+        expect(pipeline.reload).not_to be_active
+        expect(pipeline.reload).not_to be_complete
 
-          expect(builds.pluck(:name))
-            .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+        play_manual_action('production:deploy')
 
-          pipeline.builds.find_by(name: 'test:1').success
-          pipeline.builds.find_by(name: 'test:2').drop
+        expect(builds_statuses).to eq %w[success success success pending]
+        expect(pipeline.reload).to be_running
 
-          expect(builds.pluck(:name))
-            .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+        succeed_running_or_pending
 
-          Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
+        expect(builds_names).to eq %w[code:test staging:deploy staging:test
+                                      production:deploy production:test]
+        expect(builds_statuses).to eq %w[success success success success pending]
+        expect(pipeline.reload).to be_running
 
-          expect(builds.pluck(:name)).to contain_exactly(
-            'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
-        end
+        succeed_running_or_pending
+
+        expect(builds_names).to eq %w[code:test staging:deploy staging:test
+                                      production:deploy production:test]
+        expect(builds_statuses).to eq %w[success success success success success]
+        expect(pipeline.reload).to be_success
       end
     end
+  end
 
-    context 'when there are builds that are not created yet' do
-      let(:pipeline) do
-        create(:ci_pipeline, config: config)
-      end
+  context 'when second stage has only on_failure jobs' do
+    before do
+      create_build('check', stage_idx: 0)
+      create_build('build', stage_idx: 1, when: 'on_failure')
+      create_build('test', stage_idx: 2)
 
-      let(:config) do
-        { rspec: { stage: 'test', script: 'rspec' },
-          deploy: { stage: 'deploy', script: 'rsync' } }
-      end
+      process_pipeline
+    end
+
+    it 'skips second stage and continues on third stage' do
+      expect(all_builds_statuses).to eq(%w[pending created created])
 
+      builds.first.success
+
+      expect(all_builds_statuses).to eq(%w[success skipped pending])
+    end
+  end
+
+  context 'when failed build in the middle stage is retried' do
+    context 'when failed build is the only unsuccessful build in the stage' do
       before do
-        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
-        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+        create_build('build:1', stage_idx: 0)
+        create_build('build:2', stage_idx: 0)
+        create_build('test:1', stage_idx: 1)
+        create_build('test:2', stage_idx: 1)
+        create_build('deploy:1', stage_idx: 2)
+        create_build('deploy:2', stage_idx: 2)
       end
 
-      it 'processes the pipeline' do
-        # Currently we have five builds with state created
-        #
-        expect(builds.count).to eq(0)
-        expect(all_builds.count).to eq(2)
+      it 'does trigger builds in the next stage' do
+        expect(process_pipeline).to be_truthy
+        expect(builds_names).to eq ['build:1', 'build:2']
 
-        # Process builds service will enqueue builds from the first stage.
-        #
-        process_pipeline
+        succeed_running_or_pending
 
-        expect(builds.count).to eq(2)
-        expect(all_builds.count).to eq(2)
+        expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
 
-        # When builds succeed we will enqueue remaining builds.
-        #
-        # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
-        # additional build from `.gitlab-ci.yml`).
-        #
-        succeed_pending
-        process_pipeline
+        pipeline.builds.find_by(name: 'test:1').success
+        pipeline.builds.find_by(name: 'test:2').drop
 
-        expect(builds.success.count).to eq(2)
-        expect(builds.pending.count).to eq(1)
-        expect(all_builds.count).to eq(4)
+        expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
 
-        # When pending build succeeds in stage test, we enqueue deploy stage.
-        #
-        succeed_pending
-        process_pipeline
+        Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
 
-        expect(builds.pending.count).to eq(1)
-        expect(builds.success.count).to eq(3)
-        expect(all_builds.count).to eq(4)
+        expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2',
+                                    'test:2', 'deploy:1', 'deploy:2']
+      end
+    end
+  end
 
-        # When the last one succeeds we have 4 successful builds.
-        #
-        succeed_pending
-        process_pipeline
+  context 'when there are builds that are not created yet' do
+    let(:pipeline) do
+      create(:ci_pipeline, config: config)
+    end
 
-        expect(builds.success.count).to eq(4)
-        expect(all_builds.count).to eq(4)
-      end
+    let(:config) do
+      { rspec: { stage: 'test', script: 'rspec' },
+        deploy: { stage: 'deploy', script: 'rsync' } }
+    end
+
+    before do
+      create_build('linux', stage: 'build', stage_idx: 0)
+      create_build('mac', stage: 'build', stage_idx: 0)
+    end
+
+    it 'processes the pipeline' do
+      # Currently we have five builds with state created
+      #
+      expect(builds.count).to eq(0)
+      expect(all_builds.count).to eq(2)
+
+      # Process builds service will enqueue builds from the first stage.
+      #
+      process_pipeline
+
+      expect(builds.count).to eq(2)
+      expect(all_builds.count).to eq(2)
+
+      # When builds succeed we will enqueue remaining builds.
+      #
+      # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
+      # additional build from `.gitlab-ci.yml`).
+      #
+      succeed_pending
+      process_pipeline
+
+      expect(builds.success.count).to eq(2)
+      expect(builds.pending.count).to eq(1)
+      expect(all_builds.count).to eq(4)
+
+      # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
+      #
+      succeed_pending
+      process_pipeline
+
+      expect(builds.pending.count).to eq(1)
+      expect(builds.success.count).to eq(3)
+      expect(all_builds.count).to eq(4)
+
+      # When the last one succeeds we have 4 successful builds.
+      #
+      succeed_pending
+      process_pipeline
+
+      expect(builds.success.count).to eq(4)
+      expect(all_builds.count).to eq(4)
     end
   end
 
+  def process_pipeline
+    described_class.new(pipeline.project, user).execute(pipeline)
+  end
+
   def all_builds
-    pipeline.builds
+    pipeline.builds.order(:stage_idx, :id)
   end
 
   def builds
     all_builds.where.not(status: [:created, :skipped])
   end
 
-  def process_pipeline
-    described_class.new(pipeline.project, user).execute(pipeline)
+  def builds_names
+    builds.pluck(:name)
+  end
+
+  def builds_statuses
+    builds.pluck(:status)
+  end
+
+  def all_builds_statuses
+    all_builds.pluck(:status)
   end
 
   def succeed_pending
     builds.pending.update_all(status: 'success')
   end
 
-  def manual_actions
-    pipeline.manual_actions
+  def succeed_running_or_pending
+    pipeline.builds.running_or_pending.each(&:success)
+  end
+
+  def fail_running_or_pending
+    pipeline.builds.running_or_pending.each(&:drop)
+  end
+
+  def cancel_running_or_pending
+    pipeline.builds.running_or_pending.each(&:cancel)
+  end
+
+  def play_manual_action(name)
+    builds.find_by(name: name).play(user)
   end
 
-  def create_build(name, stage_idx, when_value = nil)
-    create(:ci_build,
-           :created,
-           pipeline: pipeline,
-           name: name,
-           stage_idx: stage_idx,
-           when: when_value)
+  delegate :manual_actions, to: :pipeline
+
+  def create_build(name, **opts)
+    create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
   end
 end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
similarity index 80%
rename from spec/services/ci/register_build_service_spec.rb
rename to spec/services/ci/register_job_service_spec.rb
index d9f774a1095f2b4807373a998c66b2a3afde11cd..62ba0b01339d2bf16d080a7db71814bdbb4b71b5 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 module Ci
-  describe RegisterBuildService, services: true do
+  describe RegisterJobService, services: true do
     let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
     let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
     let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
@@ -170,6 +170,51 @@ module Ci
         end
       end
 
+      context 'when first build is stalled' do
+        before do
+          pending_build.lock_version = 10
+        end
+
+        subject { described_class.new(specific_runner).execute }
+
+        context 'with multiple builds are in queue' do
+          let!(:other_build) { create :ci_build, pipeline: pipeline }
+
+          before do
+            allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+              .and_return([pending_build, other_build])
+          end
+
+          it "receives second build from the queue" do
+            expect(subject).to be_valid
+            expect(subject.build).to eq(other_build)
+          end
+        end
+
+        context 'when single build is in queue' do
+          before do
+            allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+              .and_return([pending_build])
+          end
+
+          it "does not receive any valid result" do
+            expect(subject).not_to be_valid
+          end
+        end
+
+        context 'when there is no build in queue' do
+          before do
+            allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+              .and_return([])
+          end
+
+          it "does not receive builds but result is valid" do
+            expect(subject).to be_valid
+            expect(subject.build).to be_nil
+          end
+        end
+      end
+
       def execute(runner)
         described_class.new(runner).execute.build
       end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 93147870afeb10a2e0ab517d64f417a68a64dcc8..8567817147b72f397ddb76af955c2a878cd8d743 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -10,22 +10,39 @@ describe Ci::RetryBuildService, :services do
     described_class.new(project, user)
   end
 
+  CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
+
+  REJECT_ACCESSORS =
+    %i[id status user token coverage trace runner artifacts_expire_at
+       artifacts_file artifacts_metadata artifacts_size created_at
+       updated_at started_at finished_at queued_at erased_by
+       erased_at].freeze
+
+  IGNORE_ACCESSORS =
+    %i[type lock_version target_url base_tags
+       commit_id deployments erased_by_id last_deployment project_id
+       runner_id tag_taggings taggings tags trigger_request_id
+       user_id].freeze
+
   shared_examples 'build duplication' do
     let(:build) do
-      create(:ci_build, :failed, :artifacts, :erased, :trace,
-             :queued, :coverage, pipeline: pipeline)
+      create(:ci_build, :failed, :artifacts_expired, :erased,
+             :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
+             :teardown_environment, :triggered, :trace,
+             description: 'some build', pipeline: pipeline)
     end
 
-    describe 'clone attributes' do
-      described_class::CLONE_ATTRIBUTES.each do |attribute|
+    describe 'clone accessors' do
+      CLONE_ACCESSORS.each do |attribute|
         it "clones #{attribute} build attribute" do
+          expect(new_build.send(attribute)).to be_present
           expect(new_build.send(attribute)).to eq build.send(attribute)
         end
       end
     end
 
-    describe 'reject attributes' do
-      described_class::REJECT_ATTRIBUTES.each do |attribute|
+    describe 'reject acessors' do
+      REJECT_ACCESSORS.each do |attribute|
         it "does not clone #{attribute} build attribute" do
           expect(new_build.send(attribute)).not_to eq build.send(attribute)
         end
@@ -33,12 +50,20 @@ describe Ci::RetryBuildService, :services do
     end
 
     it 'has correct number of known attributes' do
-      attributes =
-        described_class::CLONE_ATTRIBUTES +
-        described_class::IGNORE_ATTRIBUTES +
-        described_class::REJECT_ATTRIBUTES
+      known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS
+
+      # :tag_list is a special case, this accessor does not exist
+      # in reflected associations, comes from `act_as_taggable` and
+      # we use it to copy tags, instead of reusing tags.
+      #
+      current_accessors =
+        Ci::Build.attribute_names.map(&:to_sym) +
+        Ci::Build.reflect_on_all_associations.map(&:name) +
+        [:tag_list]
+
+      current_accessors.uniq!
 
-      expect(attributes.size).to eq build.attributes.size
+      expect(known_accessors).to contain_exactly(*current_accessors)
     end
   end
 
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index c0af8b8450a4a02fbb03a7a4a4dbcd9cd61bc6db..5445b65f4e8b2d95a6fafbdc7ea53e51f29e274e 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -69,35 +69,95 @@ describe Ci::RetryPipelineService, '#execute', :services do
       end
     end
 
+    context 'when the last stage was skipepd' do
+      before do
+        create_build('build 1', :success, 0)
+        create_build('test 2', :failed, 1)
+        create_build('report 3', :skipped, 2)
+        create_build('report 4', :skipped, 2)
+      end
+
+      it 'retries builds only in the first stage' do
+        service.execute(pipeline)
+
+        expect(build('build 1')).to be_success
+        expect(build('test 2')).to be_pending
+        expect(build('report 3')).to be_created
+        expect(build('report 4')).to be_created
+        expect(pipeline.reload).to be_running
+      end
+    end
+
     context 'when pipeline contains manual actions' do
-      context 'when there is a canceled manual action in first stage' do
-        before do
-          create_build('rspec 1', :failed, 0)
-          create_build('staging', :canceled, 0, :manual)
-          create_build('rspec 2', :canceled, 1)
+      context 'when there are optional manual actions only' do
+        context 'when there is a canceled manual action in first stage' do
+          before do
+            create_build('rspec 1', :failed, 0)
+            create_build('staging', :canceled, 0, when: :manual, allow_failure: true)
+            create_build('rspec 2', :canceled, 1)
+          end
+
+          it 'retries failed builds and marks subsequent for processing' do
+            service.execute(pipeline)
+
+            expect(build('rspec 1')).to be_pending
+            expect(build('staging')).to be_manual
+            expect(build('rspec 2')).to be_created
+            expect(pipeline.reload).to be_running
+          end
         end
+      end
 
-        it 'retries builds failed builds and marks subsequent for processing' do
-          service.execute(pipeline)
+      context 'when pipeline has blocking manual actions defined' do
+        context 'when pipeline retry should enqueue builds' do
+          before do
+            create_build('test', :failed, 0)
+            create_build('deploy', :canceled, 0, when: :manual, allow_failure: false)
+            create_build('verify', :canceled, 1)
+          end
+
+          it 'retries failed builds' do
+            service.execute(pipeline)
+
+            expect(build('test')).to be_pending
+            expect(build('deploy')).to be_manual
+            expect(build('verify')).to be_created
+            expect(pipeline.reload).to be_running
+          end
+        end
 
-          expect(build('rspec 1')).to be_pending
-          expect(build('staging')).to be_skipped
-          expect(build('rspec 2')).to be_created
-          expect(pipeline.reload).to be_running
+        context 'when pipeline retry should block pipeline immediately' do
+          before do
+            create_build('test', :success, 0)
+            create_build('deploy:1', :success, 1, when: :manual, allow_failure: false)
+            create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false)
+            create_build('verify', :canceled, 2)
+          end
+
+          it 'reprocesses blocking manual action and blocks pipeline' do
+            service.execute(pipeline)
+
+            expect(build('deploy:1')).to be_success
+            expect(build('deploy:2')).to be_manual
+            expect(build('verify')).to be_created
+            expect(pipeline.reload).to be_blocked
+          end
         end
       end
 
       context 'when there is a skipped manual action in last stage' do
         before do
           create_build('rspec 1', :canceled, 0)
-          create_build('staging', :skipped, 1, :manual)
+          create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true)
+          create_build('staging', :skipped, 1, when: :manual, allow_failure: true)
         end
 
-        it 'retries canceled job and skips manual action' do
+        it 'retries canceled job and reprocesses manual actions' do
           service.execute(pipeline)
 
           expect(build('rspec 1')).to be_pending
-          expect(build('staging')).to be_skipped
+          expect(build('rspec 2')).to be_manual
+          expect(build('staging')).to be_created
           expect(pipeline.reload).to be_running
         end
       end
@@ -105,7 +165,7 @@ describe Ci::RetryPipelineService, '#execute', :services do
       context 'when there is a created manual action in the last stage' do
         before do
           create_build('rspec 1', :canceled, 0)
-          create_build('staging', :created, 1, :manual)
+          create_build('staging', :created, 1, when: :manual, allow_failure: true)
         end
 
         it 'retries canceled job and does not update the manual action' do
@@ -120,14 +180,14 @@ describe Ci::RetryPipelineService, '#execute', :services do
       context 'when there is a created manual action in the first stage' do
         before do
           create_build('rspec 1', :canceled, 0)
-          create_build('staging', :created, 0, :manual)
+          create_build('staging', :created, 0, when: :manual, allow_failure: true)
         end
 
-        it 'retries canceled job and skipps the manual action' do
+        it 'retries canceled job and processes the manual action' do
           service.execute(pipeline)
 
           expect(build('rspec 1')).to be_pending
-          expect(build('staging')).to be_skipped
+          expect(build('staging')).to be_manual
           expect(pipeline.reload).to be_running
         end
       end
@@ -162,13 +222,12 @@ describe Ci::RetryPipelineService, '#execute', :services do
     statuses.latest.find_by(name: name)
   end
 
-  def create_build(name, status, stage_num, on = 'on_success')
+  def create_build(name, status, stage_num, **opts)
     create(:ci_build, name: name,
                       status: status,
                       stage: "stage_#{stage_num}",
                       stage_idx: stage_num,
-                      when: on,
-                      pipeline: pipeline) do |build|
+                      pipeline: pipeline, **opts) do |build|
       pipeline.update_status
     end
   end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 6fb4d517115f0bc6b793df52386e21c0edc06a5c..a883705bd45c8942104b9c187aa0493971e27eed 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -9,7 +9,8 @@ describe CreateDeploymentService, services: true do
   describe '#execute' do
     let(:options) { nil }
     let(:params) do
-      { environment: 'production',
+      {
+        environment: 'production',
         ref: 'master',
         tag: false,
         sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -83,10 +84,11 @@ describe CreateDeploymentService, services: true do
 
     context 'for environment with invalid name' do
       let(:params) do
-        { environment: 'name,with,commas',
+        {
+          environment: 'name,with,commas',
           ref: 'master',
           tag: false,
-          sha: '97de212e80737a608d939f648d959671fb0a0142',
+          sha: '97de212e80737a608d939f648d959671fb0a0142'
         }
       end
 
@@ -101,16 +103,17 @@ describe CreateDeploymentService, services: true do
 
     context 'when variables are used' do
       let(:params) do
-        { environment: 'review-apps/$CI_BUILD_REF_NAME',
+        {
+          environment: 'review-apps/$CI_COMMIT_REF_NAME',
           ref: 'master',
           tag: false,
           sha: '97de212e80737a608d939f648d959671fb0a0142',
           options: {
-            name: 'review-apps/$CI_BUILD_REF_NAME',
-            url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
+            name: 'review-apps/$CI_COMMIT_REF_NAME',
+            url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com'
           },
           variables: [
-            { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+            { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' }
           ]
         }
       end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 2a0f00ce93721f49e203d9a5890e7f3e81929071..bd71618e6f442f47bf41ba20063fd2b33b1dd622 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -150,6 +150,13 @@ describe GitPushService, services: true do
         execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
       end
     end
+    
+    context "Sends System Push data" do
+      it "when pushing on a branch" do
+        expect(SystemHookPushWorker).to receive(:perform_async).with(@push_data, :push_hooks)
+        execute_service(project, user, @oldrev, @newrev, @ref )
+      end
+    end
   end
 
   describe "Updates git attributes" do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 14717a7455d3de3d56ea5709b00ec163b55b5e65..ec89b540e6a11fe19687474a946a2ec6ea7d415a 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do
   let!(:user) { create(:user) }
   let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
 
+  subject { service.execute }
+
   describe 'visibility level restrictions' do
     let!(:service) { described_class.new(user, group_params) }
 
-    subject { service.execute }
-
     context "create groups without restricted visibility level" do
       it { is_expected.to be_persisted }
     end
@@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do
     let!(:group) { create(:group) }
     let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
 
-    subject { service.execute }
-
     context 'as group owner' do
       before { group.add_owner(user) }
 
@@ -40,4 +38,20 @@ describe Groups::CreateService, '#execute', services: true do
       end
     end
   end
+
+  describe 'creating a mattermost team' do
+    let!(:params) { group_params.merge(create_chat_team: "true") }
+    let!(:service) { described_class.new(user, params) }
+
+    before do
+      Settings.mattermost['enabled'] = true
+    end
+
+    it 'create the chat team with the group' do
+      allow_any_instance_of(Mattermost::Team).to receive(:create)
+        .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
+
+      expect { subject }.to change { ChatTeam.count }.from(0).to(1)
+    end
+  end
 end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 32c2ed8cae7cccaced9a96d1c6cf5ca5851f5390..98c560ffb26ba4b94e0ae2fe647e92f8855cd86c 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do
 
   let!(:user)         { create(:user) }
   let!(:group)        { create(:group) }
+  let!(:nested_group) { create(:group, parent: group) }
   let!(:project)      { create(:project, namespace: group) }
   let!(:gitlab_shell) { Gitlab::Shell.new }
   let!(:remove_path)  { group.path + "+#{group.id}+deleted" }
@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do
       end
 
       it { expect(Group.unscoped.all).not_to include(group) }
+      it { expect(Group.unscoped.all).not_to include(nested_group) }
       it { expect(Project.unscoped.all).not_to include(project) }
     end
 
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 09807e5d35ba3f268d1c21780682c78f090c2721..1dd53236fbd651af1865f545e726dcb15a161b70 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -8,24 +8,34 @@ describe Issues::BuildService, services: true do
     project.team << [user, :developer]
   end
 
+  context 'for a single discussion' do
+    describe '#execute' do
+      let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
+      let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done")]) }
+      let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
+
+      it 'references the noteable title in the issue title' do
+        issue = service.execute
+
+        expect(issue.title).to include('Hello world')
+      end
+
+      it 'adds the note content to the description' do
+        issue = service.execute
+
+        expect(issue.description).to include('Almost done')
+      end
+    end
+  end
+
   context 'for discussions in a merge request' do
     let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
-    let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute }
-
-    def position_on_line(line_number)
-      Gitlab::Diff::Position.new(
-        old_path: "files/ruby/popen.rb",
-        new_path: "files/ruby/popen.rb",
-        old_line: nil,
-        new_line: line_number,
-        diff_refs: merge_request.diff_refs
-      )
-    end
+    let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute }
 
     describe '#items_for_discussions' do
       it 'has an item for each discussion' do
-        create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13))
-        service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request)
+        create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13)
+        service = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid)
 
         service.execute
 
@@ -34,7 +44,7 @@ describe Issues::BuildService, services: true do
     end
 
     describe '#item_for_discussion' do
-      let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) }
+      let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
 
       it 'mentions the author of the note' do
         discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))])
@@ -47,11 +57,11 @@ describe Issues::BuildService, services: true do
                     "with a blockquote\n"\
                     "> That has a quote\n"\
                     ">>>\n"
-        note_result = "This is a string\n"\
-                    "> with a blockquote\n"\
-                    "> > That has a quote\n"
+        note_result = "    > This is a string\n"\
+                      "    > > with a blockquote\n"\
+                      "    > > > That has a quote\n"
         discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)])
-        expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>")
+        expect(service.item_for_discussion(discussion)).to include(note_result)
       end
     end
 
@@ -66,7 +76,7 @@ describe Issues::BuildService, services: true do
 
       it 'does not assign title when a title was given' do
         issue = described_class.new(project, user,
-                                    merge_request_for_resolving_discussions: merge_request,
+                                    merge_request_to_resolve_discussions_of: merge_request,
                                     title: 'What an issue').execute
 
         expect(issue.title).to eq('What an issue')
@@ -74,7 +84,7 @@ describe Issues::BuildService, services: true do
 
       it 'does not assign description when a description was given' do
         issue = described_class.new(project, user,
-                                    merge_request_for_resolving_discussions: merge_request,
+                                    merge_request_to_resolve_discussions_of: merge_request,
                                     description: 'Fix at your earliest conveignance').execute
 
         expect(issue.description).to eq('Fix at your earliest conveignance')
@@ -82,7 +92,7 @@ describe Issues::BuildService, services: true do
 
       describe 'with multiple discussions' do
         before do
-          create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+          create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15)
         end
 
         it 'mentions all the authors in the description' do
@@ -99,7 +109,7 @@ describe Issues::BuildService, services: true do
         end
 
         it 'mentions additional notes' do
-          create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+          create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, line_number: 15)
 
           expect(issue.description).to include('(+2 comments)')
         end
@@ -112,7 +122,7 @@ describe Issues::BuildService, services: true do
 
     describe '#execute' do
       it 'mentions the merge request in the description' do
-        issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute
+        issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute
 
         expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}")
       end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 6045d00ff097ed6bf4c6522a49780b747e0021d2..776cbc4296b0826c30679bd7a207d3a9ae06f209 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -140,46 +140,85 @@ describe Issues::CreateService, services: true do
 
     it_behaves_like 'new issuable record that supports slash commands'
 
-    context 'for a merge request' do
+    context 'resolving discussions' do
       let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
       let(:merge_request) { discussion.noteable }
       let(:project) { merge_request.source_project }
-      let(:opts) { { merge_request_for_resolving_discussions: merge_request } }
 
       before do
         project.team << [user, :master]
       end
 
-      it 'resolves the discussion for the merge request' do
-        described_class.new(project, user, opts).execute
-        discussion.first_note.reload
+      describe 'for a single discussion' do
+        let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
 
-        expect(discussion.resolved?).to be(true)
-      end
+        it 'resolves the discussion' do
+          described_class.new(project, user, opts).execute
+          discussion.first_note.reload
 
-      it 'added a system note to the discussion' do
-        described_class.new(project, user, opts).execute
+          expect(discussion.resolved?).to be(true)
+        end
 
-        reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+        it 'added a system note to the discussion' do
+          described_class.new(project, user, opts).execute
 
-        expect(reloaded_discussion.last_note.system).to eq(true)
-      end
+          reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+          expect(reloaded_discussion.last_note.system).to eq(true)
+        end
+
+        it 'assigns the title and description for the issue' do
+          issue = described_class.new(project, user, opts).execute
+
+          expect(issue.title).not_to be_nil
+          expect(issue.description).not_to be_nil
+        end
 
-      it 'assigns the title and description for the issue' do
-        issue = described_class.new(project, user, opts).execute
+        it 'can set nil explicitly to the title and description' do
+          issue = described_class.new(project, user,
+                                      merge_request_to_resolve_discussions_of: merge_request,
+                                      description: nil,
+                                      title: nil).execute
 
-        expect(issue.title).not_to be_nil
-        expect(issue.description).not_to be_nil
+          expect(issue.description).to be_nil
+          expect(issue.title).to be_nil
+        end
       end
 
-      it 'can set nil explicityly to the title and description' do
-        issue = described_class.new(project, user,
-                                    merge_request_for_resolving_discussions: merge_request,
-                                    description: nil,
-                                    title: nil).execute
+      describe 'for a merge request' do
+        let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
+
+        it 'resolves the discussion' do
+          described_class.new(project, user, opts).execute
+          discussion.first_note.reload
 
-        expect(issue.description).to be_nil
-        expect(issue.title).to be_nil
+          expect(discussion.resolved?).to be(true)
+        end
+
+        it 'added a system note to the discussion' do
+          described_class.new(project, user, opts).execute
+
+          reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+          expect(reloaded_discussion.last_note.system).to eq(true)
+        end
+
+        it 'assigns the title and description for the issue' do
+          issue = described_class.new(project, user, opts).execute
+
+          expect(issue.title).not_to be_nil
+          expect(issue.description).not_to be_nil
+        end
+
+        it 'can set nil explicitly to the title and description' do
+          issue = described_class.new(project, user,
+                                      merge_request_to_resolve_discussions_of: merge_request,
+                                      description: nil,
+                                      title: nil).execute
+
+          expect(issue.description).to be_nil
+          expect(issue.title).to be_nil
+        end
       end
     end
 
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6cc738aec080096d7be1e4bf128a8df4b2267a8b
--- /dev/null
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper.rb'
+
+class DummyService < Issues::BaseService
+  include ::Issues::ResolveDiscussions
+
+  def initialize(*args)
+    super
+    filter_resolve_discussion_params
+  end
+end
+
+describe DummyService, services: true do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  before do
+    project.team << [user, :developer]
+  end
+
+  describe "for resolving discussions" do
+    let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, note: "Almost done")]) }
+    let(:merge_request) { discussion.noteable }
+    let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
+
+    describe "#merge_request_for_resolving_discussion" do
+      let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
+
+      it "finds the merge request" do
+        expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
+      end
+
+      it "only queries for the merge request once" do
+        fake_finder = double
+        fake_results = double
+
+        expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1)
+        expect(fake_results).to receive(:find_by).exactly(1)
+        expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1)
+
+        2.times { service.merge_request_to_resolve_discussions_of }
+      end
+    end
+
+    describe "#discussions_to_resolve" do
+      it "contains a single discussion when matching merge request and discussion are passed" do
+        service = described_class.new(
+          project,
+          user,
+          discussion_to_resolve: discussion.id,
+          merge_request_to_resolve_discussions_of: merge_request.iid
+        )
+        # We need to compare discussion id's because the Discussion-objects are rebuilt
+        # which causes the object-id's not to be different.
+        discussion_ids = service.discussions_to_resolve.map(&:id)
+
+        expect(discussion_ids).to contain_exactly(discussion.id)
+      end
+
+      it "contains all discussions when only a merge request is passed" do
+        second_discussion = Discussion.new([create(:diff_note_on_merge_request,
+                                                  noteable: merge_request,
+                                                  project: merge_request.target_project,
+                                                  line_number: 15)])
+        service = described_class.new(
+          project,
+          user,
+          merge_request_to_resolve_discussions_of: merge_request.iid
+        )
+        # We need to compare discussion id's because the Discussion-objects are rebuilt
+        # which causes the object-id's not to be different.
+        discussion_ids = service.discussions_to_resolve.map(&:id)
+
+        expect(discussion_ids).to contain_exactly(discussion.id, second_discussion.id)
+      end
+
+      it "contains only unresolved discussions" do
+        _second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved,
+                                                   noteable: merge_request,
+                                                   project: merge_request.target_project,
+                                                   line_number: 15,
+                                                   )])
+        service = described_class.new(
+          project,
+          user,
+          merge_request_to_resolve_discussions_of: merge_request.iid
+        )
+        # We need to compare discussion id's because the Discussion-objects are rebuilt
+        # which causes the object-id's not to be different.
+        discussion_ids = service.discussions_to_resolve.map(&:id)
+
+        expect(discussion_ids).to contain_exactly(discussion.id)
+      end
+
+      it "is empty when a discussion and another merge request are passed" do
+        service = described_class.new(
+          project,
+          user,
+          discussion_to_resolve: discussion.id,
+          merge_request_to_resolve_discussions_of: other_merge_request.iid
+        )
+
+        expect(service.discussions_to_resolve).to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d83b09fd32ca484a3e5232fe3b4e98888848a87a..fa472f3e2c31ab09c640a9ed853d202e9cbfd59f 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -58,6 +58,22 @@ describe Issues::UpdateService, services: true do
         expect(issue.due_date).to eq Date.tomorrow
       end
 
+      it 'sorts issues as specified by parameters' do
+        issue1 = create(:issue, project: project, assignee_id: user3.id)
+        issue2 = create(:issue, project: project, assignee_id: user3.id)
+
+        [issue, issue1, issue2].each do |issue|
+          issue.move_to_end
+          issue.save
+        end
+
+        opts[:move_between_iids] = [issue1.iid, issue2.iid]
+
+        update_issue(opts)
+
+        expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+      end
+
       context 'when current user cannot admin issues in the project' do
         let(:guest) { create(:user) }
         before do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0768f644036d43c2755e0c1fe1971dac5ed05c2e..adfa75a524fb3966502a08e63cf74969c30bc812 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do
       let(:commits) { Commit.decorate([commit_1], project) }
 
       it 'creates compare object with target branch as default branch' do
-        expect(merge_request.can_be_created).to eq(false)
         expect(merge_request.compare).to be_present
         expect(merge_request.target_branch).to eq(project.default_branch)
       end
+
+      it 'allows the merge request to be created' do
+        expect(merge_request.can_be_created).to eq(true)
+      end
     end
 
     context 'same source and target branch' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 08829e4be70589493381a75119117fe0688e56d3..b7a0590720893d247c814e0fbea08b32abaffaa2 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -130,5 +130,15 @@ describe MergeRequests::GetUrlsService do
         }])
       end
     end
+
+    context 'when printing_merge_request_link_enabled is false' do
+      it 'returns empty array' do
+        project.update!(printing_merge_request_link_enabled: false)
+
+        result = service.execute(existing_branch_changes)
+
+        expect(result).to eq([])
+      end
+    end
   end
 end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index f92978a33a3dabce6990b9d818e991fa018e078e..c2f205c389dc0d76f43d1d4a1ac09bf8fca752a2 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
   let(:project) { create(:project) }
 
   let(:mr_merge_if_green_enabled) do
-    create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
+    create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
                            source_branch: "master", target_branch: 'feature',
                            source_project: project, target_project: project, state: "opened")
   end
@@ -36,7 +36,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
 
       it 'sets the params, merge_user, and flag' do
         expect(merge_request).to be_valid
-        expect(merge_request.merge_when_build_succeeds).to be_truthy
+        expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
         expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
         expect(merge_request.merge_user).to be user
       end
@@ -62,7 +62,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
       end
 
       it 'updates the merge params' do
-        expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
+        expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
 
         service.execute(mr_merge_if_green_enabled)
         expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
@@ -82,7 +82,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
                              sha: merge_request_head, status: 'success')
       end
 
-      it "merges all merge requests with merge when build succeeds enabled" do
+      it "merges all merge requests with merge when the pipeline succeeds enabled" do
         expect(MergeWorker).to receive(:perform_async)
         service.trigger(triggering_pipeline)
       end
@@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
         service.trigger(unrelated_pipeline)
       end
     end
+
+    context 'when the merge request is not mergeable' do
+      let(:mr_conflict) do
+        create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
+                               source_branch: 'master', target_branch: 'feature-conflict',
+                               source_project: project, target_project: project)
+      end
+
+      let(:conflict_pipeline) do
+        create(:ci_pipeline, project: project, ref: mr_conflict.source_branch,
+                             sha: mr_conflict.diff_head_sha, status: 'success')
+      end
+
+      it 'does not merge the merge request' do
+        expect(MergeWorker).not_to receive(:perform_async)
+
+        service.trigger(conflict_pipeline)
+      end
+
+      it 'creates todos for unmergeability' do
+        expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict)
+
+        service.trigger(conflict_pipeline)
+      end
+    end
   end
 
   describe "#cancel" do
@@ -118,8 +143,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
       service.cancel(mr_merge_if_green_enabled)
     end
 
-    it "resets all the merge_when_build_succeeds params" do
-      expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+    it "resets all the pipeline succeeds params" do
+      expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey
       expect(mr_merge_if_green_enabled.merge_params).to eq({})
       expect(mr_merge_if_green_enabled.merge_user).to be nil
     end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 983dac6efdb10101b9ccf56c70654f09575e562f..92729f68e5fef164ceb254d6c312980f2eb3949e 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -18,7 +18,7 @@ describe MergeRequests::RefreshService, services: true do
                               source_branch: 'master',
                               target_branch: 'feature',
                               target_project: @project,
-                              merge_when_build_succeeds: true,
+                              merge_when_pipeline_succeeds: true,
                               merge_user: @user)
 
       @fork_merge_request = create(:merge_request,
@@ -58,16 +58,16 @@ describe MergeRequests::RefreshService, services: true do
       it 'executes hooks with update action' do
         expect(refresh_service).to have_received(:execute_hooks).
           with(@merge_request, 'update', @oldrev)
-      end
 
-      it { expect(@merge_request.notes).not_to be_empty }
-      it { expect(@merge_request).to be_open }
-      it { expect(@merge_request.merge_when_build_succeeds).to be_falsey }
-      it { expect(@merge_request.diff_head_sha).to eq(@newrev) }
-      it { expect(@fork_merge_request).to be_open }
-      it { expect(@fork_merge_request.notes).to be_empty }
-      it { expect(@build_failed_todo).to be_done }
-      it { expect(@fork_build_failed_todo).to be_done }
+        expect(@merge_request.notes).not_to be_empty
+        expect(@merge_request).to be_open
+        expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+        expect(@merge_request.diff_head_sha).to eq(@newrev)
+        expect(@fork_merge_request).to be_open
+        expect(@fork_merge_request.notes).to be_empty
+        expect(@build_failed_todo).to be_done
+        expect(@fork_build_failed_todo).to be_done
+      end
     end
 
     context 'push to origin repo target branch' do
@@ -76,12 +76,14 @@ describe MergeRequests::RefreshService, services: true do
         reload_mrs
       end
 
-      it { expect(@merge_request.notes.last.note).to include('merged') }
-      it { expect(@merge_request).to be_merged }
-      it { expect(@fork_merge_request).to be_merged }
-      it { expect(@fork_merge_request.notes.last.note).to include('merged') }
-      it { expect(@build_failed_todo).to be_done }
-      it { expect(@fork_build_failed_todo).to be_done }
+      it 'updates the merge state' do
+        expect(@merge_request.notes.last.note).to include('merged')
+        expect(@merge_request).to be_merged
+        expect(@fork_merge_request).to be_merged
+        expect(@fork_merge_request.notes.last.note).to include('merged')
+        expect(@build_failed_todo).to be_done
+        expect(@fork_build_failed_todo).to be_done
+      end
     end
 
     context 'manual merge of source branch' do
@@ -95,13 +97,15 @@ describe MergeRequests::RefreshService, services: true do
         reload_mrs
       end
 
-      it { expect(@merge_request.notes.last.note).to include('merged') }
-      it { expect(@merge_request).to be_merged }
-      it { expect(@merge_request.diffs.size).to be > 0 }
-      it { expect(@fork_merge_request).to be_merged }
-      it { expect(@fork_merge_request.notes.last.note).to include('merged') }
-      it { expect(@build_failed_todo).to be_done }
-      it { expect(@fork_build_failed_todo).to be_done }
+      it 'updates the merge state' do
+        expect(@merge_request.notes.last.note).to include('merged')
+        expect(@merge_request).to be_merged
+        expect(@merge_request.diffs.size).to be > 0
+        expect(@fork_merge_request).to be_merged
+        expect(@fork_merge_request.notes.last.note).to include('merged')
+        expect(@build_failed_todo).to be_done
+        expect(@fork_build_failed_todo).to be_done
+      end
     end
 
     context 'push to fork repo source branch' do
@@ -117,14 +121,14 @@ describe MergeRequests::RefreshService, services: true do
         it 'executes hooks with update action' do
           expect(refresh_service).to have_received(:execute_hooks).
             with(@fork_merge_request, 'update', @oldrev)
-        end
 
-        it { expect(@merge_request.notes).to be_empty }
-        it { expect(@merge_request).to be_open }
-        it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
-        it { expect(@fork_merge_request).to be_open }
-        it { expect(@build_failed_todo).to be_pending }
-        it { expect(@fork_build_failed_todo).to be_pending }
+          expect(@merge_request.notes).to be_empty
+          expect(@merge_request).to be_open
+          expect(@fork_merge_request.notes.last.note).to include('added 28 commits')
+          expect(@fork_merge_request).to be_open
+          expect(@build_failed_todo).to be_pending
+          expect(@fork_build_failed_todo).to be_pending
+        end
       end
 
       context 'closed fork merge request' do
@@ -139,12 +143,14 @@ describe MergeRequests::RefreshService, services: true do
           expect(refresh_service).not_to have_received(:execute_hooks)
         end
 
-        it { expect(@merge_request.notes).to be_empty }
-        it { expect(@merge_request).to be_open }
-        it { expect(@fork_merge_request.notes).to be_empty }
-        it { expect(@fork_merge_request).to be_closed }
-        it { expect(@build_failed_todo).to be_pending }
-        it { expect(@fork_build_failed_todo).to be_pending }
+        it 'updates merge request to closed state' do
+          expect(@merge_request.notes).to be_empty
+          expect(@merge_request).to be_open
+          expect(@fork_merge_request.notes).to be_empty
+          expect(@fork_merge_request).to be_closed
+          expect(@build_failed_todo).to be_pending
+          expect(@fork_build_failed_todo).to be_pending
+        end
       end
     end
 
@@ -155,12 +161,14 @@ describe MergeRequests::RefreshService, services: true do
           reload_mrs
         end
 
-        it { expect(@merge_request.notes).to be_empty }
-        it { expect(@merge_request).to be_open }
-        it { expect(@fork_merge_request.notes).to be_empty }
-        it { expect(@fork_merge_request).to be_open }
-        it { expect(@build_failed_todo).to be_pending }
-        it { expect(@fork_build_failed_todo).to be_pending }
+        it 'updates the merge request state' do
+          expect(@merge_request.notes).to be_empty
+          expect(@merge_request).to be_open
+          expect(@fork_merge_request.notes).to be_empty
+          expect(@fork_merge_request).to be_open
+          expect(@build_failed_todo).to be_pending
+          expect(@fork_build_failed_todo).to be_pending
+        end
       end
 
       describe 'merge request diff' do
@@ -179,12 +187,14 @@ describe MergeRequests::RefreshService, services: true do
         reload_mrs
       end
 
-      it { expect(@merge_request.notes.last.note).to include('merged') }
-      it { expect(@merge_request).to be_merged }
-      it { expect(@fork_merge_request).to be_open }
-      it { expect(@fork_merge_request.notes).to be_empty }
-      it { expect(@build_failed_todo).to be_done }
-      it { expect(@fork_build_failed_todo).to be_done }
+      it 'updates the merge request state' do
+        expect(@merge_request.notes.last.note).to include('merged')
+        expect(@merge_request).to be_merged
+        expect(@fork_merge_request).to be_open
+        expect(@fork_merge_request.notes).to be_empty
+        expect(@build_failed_todo).to be_done
+        expect(@fork_build_failed_todo).to be_done
+      end
     end
 
     context 'push new branch that exists in a merge request' do
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index a0e51681725b8530ef4ca211d2df318abc036300..d33535d22af83a945cb131cc883c8013eec88a94 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -59,20 +59,19 @@ describe MergeRequests::ResolveService do
 
         it 'creates a commit with the correct parents' do
           expect(merge_request.source_branch_head.parents.map(&:id)).
-            to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
-                   '824be604a34828eb682305f0d963056cfac87b2d'])
+            to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+                     824be604a34828eb682305f0d963056cfac87b2d))
         end
       end
 
       context 'when the source project is a fork and does not contain the HEAD of the target branch' do
         let!(:target_head) do
-          project.repository.commit_file(
+          project.repository.create_file(
             user,
             'new-file-in-target',
             '',
             message: 'Add new file in target',
-            branch_name: 'conflict-start',
-            update: false)
+            branch_name: 'conflict-start')
         end
 
         before do
@@ -125,8 +124,8 @@ describe MergeRequests::ResolveService do
 
       it 'creates a commit with the correct parents' do
         expect(merge_request.source_branch_head.parents.map(&:id)).
-          to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
-                 '824be604a34828eb682305f0d963056cfac87b2d'])
+          to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+                   824be604a34828eb682305f0d963056cfac87b2d))
       end
 
       it 'sets the content to the content given' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 9c92a5080c68584f33500088511e85acc92f05f5..152c6d20daac498e71632fc5e189a272987cfae7 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -102,47 +102,19 @@ describe Notes::CreateService, services: true do
         expect(subject.note).to eq(params[:note])
       end
     end
-  end
-
-  describe "award emoji" do
-    before do
-      project.team << [user, :master]
-    end
-
-    it "creates an award emoji" do
-      opts = {
-        note: ':smile: ',
-        noteable_type: 'Issue',
-        noteable_id: issue.id
-      }
-      note = described_class.new(project, user, opts).execute
-
-      expect(note).to be_valid
-      expect(note.name).to eq('smile')
-    end
 
-    it "creates regular note if emoji name is invalid" do
-      opts = {
-        note: ':smile: moretext:',
-        noteable_type: 'Issue',
-        noteable_id: issue.id
-      }
-      note = described_class.new(project, user, opts).execute
-
-      expect(note).to be_valid
-      expect(note.note).to eq(opts[:note])
-    end
-
-    it "normalizes the emoji name" do
-      opts = {
-        note: ':+1:',
-        noteable_type: 'Issue',
-        noteable_id: issue.id
-      }
-
-      expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
+    describe 'note with emoji only' do
+      it 'creates regular note' do
+        opts = {
+          note: ':smile: ',
+          noteable_type: 'Issue',
+          noteable_id: issue.id
+        }
+        note = described_class.new(project, user, opts).execute
 
-      described_class.new(project, user, opts).execute
+        expect(note).to be_valid
+        expect(note.note).to eq(':smile:')
+      end
     end
   end
 end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 839250b7d848a58cb0b72f4689f7733ec9292190..f7240969588ddf196768fd9fbd0c7baa664166fc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -146,16 +146,6 @@ describe NotificationService, services: true do
           should_not_email(@u_lazy_participant)
         end
 
-        it "emails the note author if they've opted into notifications about their activity" do
-          add_users_with_subscription(note.project, issue)
-          note.author.notified_of_own_activity = true
-          reset_delivered_emails!
-
-          notification.new_note(note)
-
-          should_email(note.author)
-        end
-
         it 'filters out "mentioned in" notes' do
           mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author)
 
@@ -486,20 +476,6 @@ describe NotificationService, services: true do
         should_not_email(issue.assignee)
       end
 
-      it "emails the author if they've opted into notifications about their activity" do
-        issue.author.notified_of_own_activity = true
-
-        notification.new_issue(issue, issue.author)
-
-        should_email(issue.author)
-      end
-
-      it "doesn't email the author if they haven't opted into notifications about their activity" do
-        notification.new_issue(issue, issue.author)
-
-        should_not_email(issue.author)
-      end
-
       it "emails subscribers of the issue's labels" do
         user_1 = create(:user)
         user_2 = create(:user)
@@ -689,19 +665,6 @@ describe NotificationService, services: true do
         should_email(subscriber_to_label_2)
       end
 
-      it "emails the current user if they've opted into notifications about their activity" do
-        subscriber_to_label_2.notified_of_own_activity = true
-        notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
-        should_email(subscriber_to_label_2)
-      end
-
-      it "doesn't email the current user if they haven't opted into notifications about their activity" do
-        notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
-        should_not_email(subscriber_to_label_2)
-      end
-
       it "doesn't send email to anyone but subscribers of the given labels" do
         notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
 
@@ -795,7 +758,7 @@ describe NotificationService, services: true do
         update_custom_notification(:reopen_issue, @u_custom_global)
       end
 
-      it 'sends email to issue assignee and issue author' do
+      it 'sends email to issue notification recipients' do
         notification.reopen_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
@@ -809,6 +772,7 @@ describe NotificationService, services: true do
         should_email(@watcher_and_subscriber)
         should_not_email(@unsubscriber)
         should_not_email(@u_participating)
+        should_not_email(@u_disabled)
         should_not_email(@u_lazy_participant)
       end
 
@@ -818,6 +782,32 @@ describe NotificationService, services: true do
         let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
       end
     end
+
+    describe '#issue_moved' do
+      let(:new_issue) { create(:issue) }
+
+      it 'sends email to issue notification recipients' do
+        notification.issue_moved(issue, new_issue, @u_disabled)
+
+        should_email(issue.assignee)
+        should_email(issue.author)
+        should_email(@u_watcher)
+        should_email(@u_guest_watcher)
+        should_email(@u_participant_mentioned)
+        should_email(@subscriber)
+        should_email(@watcher_and_subscriber)
+        should_not_email(@unsubscriber)
+        should_not_email(@u_participating)
+        should_not_email(@u_disabled)
+        should_not_email(@u_lazy_participant)
+      end
+
+      it_behaves_like 'participating notifications' do
+        let(:participant) { create(:user, username: 'user-participant') }
+        let(:issuable) { issue }
+        let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+      end
+    end
   end
 
   describe 'Merge Requests' do
@@ -855,20 +845,6 @@ describe NotificationService, services: true do
         should_not_email(@u_lazy_participant)
       end
 
-      it "emails the author if they've opted into notifications about their activity" do
-        merge_request.author.notified_of_own_activity = true
-
-        notification.new_merge_request(merge_request, merge_request.author)
-
-        should_email(merge_request.author)
-      end
-
-      it "doesn't email the author if they haven't opted into notifications about their activity" do
-        notification.new_merge_request(merge_request, merge_request.author)
-
-        should_not_email(merge_request.author)
-      end
-
       it "emails subscribers of the merge request's labels" do
         user_1 = create(:user)
         user_2 = create(:user)
@@ -1050,28 +1026,20 @@ describe NotificationService, services: true do
         should_not_email(@u_lazy_participant)
       end
 
-      it "notifies the merger when merge_when_build_succeeds is true" do
-        merge_request.merge_when_build_succeeds = true
+      it "notifies the merger when the pipeline succeeds is true" do
+        merge_request.merge_when_pipeline_succeeds = true
         notification.merge_mr(merge_request, @u_watcher)
 
         should_email(@u_watcher)
       end
 
-      it "does not notify the merger when merge_when_build_succeeds is false" do
-        merge_request.merge_when_build_succeeds = false
+      it "does not notify the merger when the pipeline succeeds is false" do
+        merge_request.merge_when_pipeline_succeeds = false
         notification.merge_mr(merge_request, @u_watcher)
 
         should_not_email(@u_watcher)
       end
 
-      it "notifies the merger when merge_when_build_succeeds is false but they've opted into notifications about their activity" do
-        merge_request.merge_when_build_succeeds = false
-        @u_watcher.notified_of_own_activity = true
-        notification.merge_mr(merge_request, @u_watcher)
-
-        should_email(@u_watcher)
-      end
-
       it_behaves_like 'participating notifications' do
         let(:participant) { create(:user, username: 'user-participant') }
         let(:issuable) { merge_request }
@@ -1251,6 +1219,48 @@ describe NotificationService, services: true do
     end
   end
 
+  describe 'Pipelines' do
+    describe '#pipeline_finished' do
+      let(:project) { create(:project, :public) }
+      let(:current_user) { create(:user) }
+      let(:u_member) { create(:user) }
+      let(:u_other) { create(:user) }
+
+      let(:commit) { project.commit }
+      let(:pipeline) do
+        create(:ci_pipeline, :success,
+               project: project,
+               user: current_user,
+               ref: 'refs/heads/master',
+               sha: commit.id,
+               before_sha: '00000000')
+      end
+
+      before do
+        project.add_master(current_user)
+        project.add_master(u_member)
+        reset_delivered_emails!
+      end
+
+      context 'without custom recipients' do
+        it 'notifies the pipeline user' do
+          notification.pipeline_finished(pipeline)
+
+          should_only_email(current_user, kind: :bcc)
+        end
+      end
+
+      context 'with custom recipients' do
+        it 'notifies the custom recipients' do
+          users = [u_member, u_other]
+          notification.pipeline_finished(pipeline, users.map(&:notification_email))
+
+          should_only_email(*users, kind: :bcc)
+        end
+      end
+    end
+  end
+
   def build_team(project)
     @u_watcher               = create_global_setting_for(create(:user), :watch)
     @u_participating         = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index af515ad2e0e02be2dfb21151874bc4b52cca52ac..62f21049b0bf7936ad982e9585e90112f6855341 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -50,7 +50,7 @@ describe Projects::CreateService, '#execute', services: true do
 
   context 'error handling' do
     it 'handles invalid options' do
-      opts.merge!({ default_branch: 'master' } )
+      opts[:default_branch] = 'master'
       expect(create_project(user, opts)).to eq(nil)
     end
   end
@@ -67,7 +67,7 @@ describe Projects::CreateService, '#execute', services: true do
 
     context 'wiki_enabled false does not create wiki repository directory' do
       it do
-        opts.merge!(wiki_enabled: false)
+        opts[:wiki_enabled] = false
         project = create_project(user, opts)
         path = ProjectWiki.new(project, user).send(:path_to_repo)
 
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ab6e8f537bac91e1f6d89c55adf0c848eacae554..e5917bb0b7a165c34b1baf69fde6d96afda8cde9 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do
       end
     end
 
+    context 'with blocked import_URL' do
+      it 'fails with localhost' do
+        project.import_url = 'https://localhost:9000/vim/vim.git'
+
+        result = described_class.new(project, user).execute
+
+        expect(result[:status]).to eq :error
+        expect(result[:message]).to end_with 'Blocked import URL.'
+      end
+
+      it 'fails with port 25' do
+        project.import_url = "https://github.com:25/vim/vim.git"
+
+        result = described_class.new(project, user).execute
+
+        expect(result[:status]).to eq :error
+        expect(result[:message]).to end_with 'Blocked import URL.'
+      end
+    end
+
     def stub_github_omniauth_provider
       provider = OpenStruct.new(
         'name' => 'github',
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 411b22a0fb83d5d198b71c05e3014fd6efdf64fa..f75fdd9e03f2defde3f06b5e816a3a76bd750e6e 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -26,6 +26,28 @@ describe Projects::UpdatePagesService do
         build.update_attributes(artifacts_metadata: metadata)
       end
 
+      describe 'pages artifacts' do
+        context 'with expiry date' do
+          before do
+            build.artifacts_expire_in = "2 days"
+          end
+
+          it "doesn't delete artifacts" do
+            expect(execute).to eq(:success)
+
+            expect(build.reload.artifacts_file?).to eq(true)
+          end
+        end
+
+        context 'without expiry date' do
+          it "does delete artifacts" do
+            expect(execute).to eq(:success)
+
+            expect(build.reload.artifacts_file?).to eq(false)
+          end
+        end
+      end
+
       it 'succeeds' do
         expect(project.pages_deployed?).to be_falsey
         expect(execute).to eq(:success)
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index c42eeba4b9ca5a4ef25ee7a9d6e1ef7d52992c1d..150c8ccaef75ed8d0d5625cea5118522b71e305b 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -10,7 +10,7 @@ describe Projects::UploadService, services: true do
     context 'for valid gif file' do
       before do
         gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
-        @link_to_file = upload_file(@project.repository, gif)
+        @link_to_file = upload_file(@project, gif)
       end
 
       it { expect(@link_to_file).to have_key(:alt) }
@@ -23,7 +23,7 @@ describe Projects::UploadService, services: true do
       before do
         png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
           'image/png')
-        @link_to_file = upload_file(@project.repository, png)
+        @link_to_file = upload_file(@project, png)
       end
 
       it { expect(@link_to_file).to have_key(:alt) }
@@ -35,7 +35,7 @@ describe Projects::UploadService, services: true do
     context 'for valid jpg file' do
       before do
         jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
-        @link_to_file = upload_file(@project.repository, jpg)
+        @link_to_file = upload_file(@project, jpg)
       end
 
       it { expect(@link_to_file).to have_key(:alt) }
@@ -47,7 +47,7 @@ describe Projects::UploadService, services: true do
     context 'for txt file' do
       before do
         txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
-        @link_to_file = upload_file(@project.repository, txt)
+        @link_to_file = upload_file(@project, txt)
       end
 
       it { expect(@link_to_file).to have_key(:alt) }
@@ -60,14 +60,14 @@ describe Projects::UploadService, services: true do
       before do
         txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
         allow(txt).to receive(:size) { 1000.megabytes.to_i }
-        @link_to_file = upload_file(@project.repository, txt)
+        @link_to_file = upload_file(@project, txt)
       end
 
       it { expect(@link_to_file).to eq(nil) }
     end
   end
 
-  def upload_file(repository, file)
-    Projects::UploadService.new(repository, file).execute
+  def upload_file(project, file)
+    Projects::UploadService.new(project, file).execute
   end
 end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 7d4eff3b6ef6c14c3f9c5cef4c749cdfa4df3d7c..6ea8f3099813dff87339cc92d1e9da8b9b8ae28a 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -6,8 +6,8 @@ describe ProtectedBranches::CreateService, services: true do
   let(:params) do
     {
       name: 'master',
-      merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
-      push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+      merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }],
+      push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
     }
   end
 
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 0b0925983eb87f5db6c4d9e09c708e5a0be15814..52e8678cb9dddb0202787f5567f0e5d6f3abe712 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -267,6 +267,14 @@ describe SlashCommands::InterpretService, services: true do
       end
     end
 
+    shared_examples 'award command' do
+      it 'toggle award 100 emoji if content containts /award :100:' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(emoji_award: "100")
+      end
+    end
+
     it_behaves_like 'reopen command' do
       let(:content) { '/reopen' }
       let(:issuable) { issue }
@@ -654,6 +662,37 @@ describe SlashCommands::InterpretService, services: true do
       end
     end
 
+    context '/award command' do
+      it_behaves_like 'award command' do
+        let(:content) { '/award :100:' }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'award command' do
+        let(:content) { '/award :100:' }
+        let(:issuable) { merge_request }
+      end
+
+      context 'ignores command with no argument' do
+        it_behaves_like 'empty command' do
+          let(:content) { '/award' }
+          let(:issuable) { issue }
+        end
+      end
+
+      context 'ignores non-existing / invalid  emojis' do
+        it_behaves_like 'empty command' do
+          let(:content) { '/award noop' }
+          let(:issuable) { issue }
+        end
+
+        it_behaves_like 'empty command' do
+          let(:content) { '/award :lorem_ipsum:' }
+          let(:issuable) { issue }
+        end
+      end
+    end
+
     context '/target_branch command' do
       let(:non_empty_project) { create(:project) }
       let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index db9f1231682a7c328aa5c0fc1fe7058c7f28cc47..11037a4917b8aa1880e5b19e8a20ffbf9725cc04 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -5,6 +5,7 @@ describe SystemHooksService, services: true do
   let(:project)       { create :project }
   let(:project_member) { create :project_member }
   let(:key)           { create(:key, user: user) }
+  let(:deploy_key)    { create(:key) }
   let(:group)         { create(:group) }
   let(:group_member)  { create(:group_member) }
 
@@ -18,6 +19,8 @@ describe SystemHooksService, services: true do
     it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
     it { expect(event_data(key, :create)).to include(:username, :key, :id) }
     it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
+    it { expect(event_data(deploy_key, :create)).to include(:key, :id) }
+    it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) }
 
     it do
       project.old_path_with_namespace = 'renamed_from_path'
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 7f027ae02a2360dc2d6ecb9b02e2e8ede1ecd087..36a17a3bf2ea280205c6fd9bf65c7f7cadda1db6 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -215,13 +215,13 @@ describe SystemNoteService, services: true do
     end
   end
 
-  describe '.merge_when_build_succeeds' do
+  describe '.merge_when_pipeline_succeeds' do
     let(:pipeline) { build(:ci_pipeline_without_jobs )}
     let(:noteable) do
       create(:merge_request, source_project: project, target_project: project)
     end
 
-    subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) }
+    subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) }
 
     it_behaves_like 'a system note'
 
@@ -230,12 +230,12 @@ describe SystemNoteService, services: true do
     end
   end
 
-  describe '.cancel_merge_when_build_succeeds' do
+  describe '.cancel_merge_when_pipeline_succeeds' do
     let(:noteable) do
       create(:merge_request, source_project: project, target_project: project)
     end
 
-    subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
+    subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) }
 
     it_behaves_like 'a system note'
 
@@ -418,45 +418,6 @@ describe SystemNoteService, services: true do
           to be_truthy
       end
     end
-
-    context 'when noteable is an Issue' do
-      let(:issue) { create(:issue, project: project) }
-
-      it 'is truthy when issue is closed' do
-        issue.close
-
-        expect(described_class.cross_reference_disallowed?(issue, project.commit)).
-          to be_truthy
-      end
-
-      it 'is falsey when issue is open' do
-        expect(described_class.cross_reference_disallowed?(issue, project.commit)).
-          to be_falsy
-      end
-    end
-
-    context 'when noteable is a Merge Request' do
-      let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
-      it 'is truthy when merge request is closed' do
-        allow(merge_request).to receive(:closed?).and_return(:true)
-
-        expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
-          to be_truthy
-      end
-
-      it 'is truthy when merge request is merged' do
-        allow(merge_request).to receive(:closed?).and_return(:true)
-
-        expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
-          to be_truthy
-      end
-
-      it 'is falsey when merge request is open' do
-        expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
-          to be_falsy
-      end
-    end
   end
 
   describe '.cross_reference_exists?' do
@@ -631,7 +592,7 @@ describe SystemNoteService, services: true do
       jira_service_settings
     end
 
-    noteable_types = ["merge_requests", "commit"]
+    noteable_types = %w(merge_requests commit)
 
     noteable_types.each do |type|
       context "when noteable is a #{type}" do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 9f24cc0f3f230fde10873c1d41cd8108b536bcde..3645b73b039973c397491d15ebf8a670f0985493 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -298,6 +298,10 @@ describe TodoService, services: true do
         expect(second_todo.reload.state?(new_state)).to be true
       end
 
+      it 'returns the updated ids' do
+        expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id])
+      end
+
       describe 'cached counts' do
         it 'updates when todos change' do
           expect(john_doe.todos.where(state: new_state).count).to eq(0)
@@ -680,7 +684,7 @@ describe TodoService, services: true do
       end
 
       it 'creates a pending todo for merge_user' do
-        mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+        mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
         service.merge_request_build_failed(mr_unassigned)
 
         should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
@@ -700,13 +704,13 @@ describe TodoService, services: true do
 
     describe '#merge_request_became_unmergeable' do
       it 'creates a pending todo for a merge_user' do
-        mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+        mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
         service.merge_request_became_unmergeable(mr_unassigned)
 
         should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
       end
     end
-    
+
     describe '#mark_todo' do
       it 'creates a todo from a merge request' do
         service.mark_todo(mr_unassigned, author)
@@ -752,7 +756,7 @@ describe TodoService, services: true do
     issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
 
     expect(john_doe.todos_pending_count).to eq(0)
-    expect(john_doe).to receive(:update_todos_count_cache)
+    expect(john_doe).to receive(:update_todos_count_cache).and_call_original
 
     service.new_issue(issue, author)
 
@@ -779,29 +783,27 @@ describe TodoService, services: true do
         .to change { todo.reload.state }.from('pending').to('done')
     end
 
-    it 'returns the number of updated todos' do # Needed on API
+    it 'returns the ids of updated todos' do # Needed on API
       todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
 
-      expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+      expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id])
     end
 
     context 'when some of the todos are done already' do
-      before do
-        create(:todo, :mentioned, user: john_doe, target: issue, project: project)
-        create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
-      end
+      let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
+      let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
 
-      it 'returns the number of those still pending' do
+      it 'returns the ids of those still pending' do
         TodoService.new.mark_pending_todos_as_done(issue, john_doe)
 
-        expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+        expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
       end
 
-      it 'returns 0 if all are done' do
+      it 'returns an empty array if all are done' do
         TodoService.new.mark_pending_todos_as_done(issue, john_doe)
         TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
 
-        expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+        expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
       end
     end
 
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
index c0bf27c698cf9c2dd5715dafdb23a880201b173a..922e82445d09cbd2d564ff63653cccef90e83ce3 100644
--- a/spec/services/users/destroy_spec.rb
+++ b/spec/services/users/destroy_spec.rb
@@ -24,6 +24,54 @@ describe Users::DestroyService, services: true do
       end
     end
 
+    context "a deleted user's issues" do
+      let(:project) { create :project }
+
+      before do
+        project.add_developer(user)
+      end
+
+      context "for an issue the user has created" do
+        let!(:issue) { create(:issue, project: project, author: user) }
+
+        before do
+          service.execute(user)
+        end
+
+        it 'does not delete the issue' do
+          expect(Issue.find_by_id(issue.id)).to be_present
+        end
+
+        it 'migrates the issue so that the "Ghost User" is the issue owner' do
+          migrated_issue = Issue.find_by_id(issue.id)
+
+          expect(migrated_issue.author).to eq(User.ghost)
+        end
+
+        it 'blocks the user before migrating issues to the "Ghost User' do
+          expect(user).to be_blocked
+        end
+      end
+
+      context "for an issue the user was assigned to" do
+        let!(:issue) { create(:issue, project: project, assignee: user) }
+
+        before do
+          service.execute(user)
+        end
+
+        it 'does not delete issues the user is assigned to' do
+          expect(Issue.find_by_id(issue.id)).to be_present
+        end
+
+        it 'migrates the issue so that it is "Unassigned"' do
+          migrated_issue = Issue.find_by_id(issue.id)
+
+          expect(migrated_issue.assignee).to be_nil
+        end
+      end
+    end
+
     context "solo owned groups present" do
       let(:solo_owned)  { create(:group) }
       let(:member)      { create(:group_member) }
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 690fe9794928ac5f1f90b6bb1370f85022a17a10..08733d6dcf150dabb6dd8b4a200d57b69d01f7ef 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do
     it 'sets the values to the access levels' do
       expect(hash.values).to eq([Gitlab::Access::MASTER])
     end
+
+    context 'personal projects' do
+      it 'includes the project with the right access level' do
+        expect(hash[project.id]).to eq(Gitlab::Access::MASTER)
+      end
+    end
+
+    context 'projects the user is a member of' do
+      let!(:other_project) { create(:empty_project) }
+
+      before do
+        other_project.team.add_reporter(user)
+      end
+
+      it 'includes the project with the right access level' do
+        expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER)
+      end
+    end
+
+    context 'projects of groups the user is a member of' do
+      let(:group) { create(:group) }
+      let!(:other_project) { create(:project, group: group) }
+
+      before do
+        group.add_owner(user)
+      end
+
+      it 'includes the project with the right access level' do
+        expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER)
+      end
+    end
+
+    context 'projects of subgroups of groups the user is a member of' do
+      let(:group) { create(:group) }
+      let(:nested_group) { create(:group, parent: group) }
+      let!(:other_project) { create(:project, group: nested_group) }
+
+      before do
+        group.add_master(user)
+      end
+
+      it 'includes the project with the right access level' do
+        expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER)
+      end
+    end
+
+    context 'projects shared with groups the user is a member of' do
+      let(:group) { create(:group) }
+      let(:other_project) { create(:empty_project) }
+      let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) }
+
+      before do
+        group.add_master(user)
+      end
+
+      it 'includes the project with the right access level' do
+        expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST)
+      end
+    end
+
+    context 'projects shared with subgroups of groups the user is a member of' do
+      let(:group) { create(:group) }
+      let(:nested_group) { create(:group, parent: group) }
+      let(:other_project) { create(:empty_project) }
+      let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) }
+
+      before do
+        group.add_master(user)
+      end
+
+      it 'includes the project with the right access level' do
+        expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER)
+      end
+    end
   end
 
   describe '#current_authorizations_per_project' do
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index b507d38f472c719e50595ca1db74a464312bb2ae..ac2c89b3ff98bcb80810774cbd2af16f033276f2 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -15,9 +15,9 @@ module SimpleCovEnv
 
   def configure_job
     SimpleCov.configure do
-      if ENV['CI_BUILD_NAME']
-        coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}"
-        command_name ENV['CI_BUILD_NAME']
+      if ENV['CI_JOB_NAME']
+        coverage_dir "coverage/#{ENV['CI_JOB_NAME']}"
+        command_name ENV['CI_JOB_NAME']
       end
 
       if ENV['CI']
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5fda7c63cdb32475689c28d79c36bc07d27366f8..ceb3209331fbd109e0146e8ca3efb3bb569bf600 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -43,14 +43,27 @@ RSpec.configure do |config|
   config.include ActiveSupport::Testing::TimeHelpers
   config.include StubGitlabCalls
   config.include StubGitlabData
+  config.include ApiHelpers, :api
 
   config.infer_spec_type_from_file_location!
+
+  config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata|
+    metadata[:api] = true
+  end
+
   config.raise_errors_for_deprecations!
 
   config.before(:suite) do
     TestEnv.init
   end
 
+  if ENV['CI']
+    # Retry only on feature specs that use JS
+    config.around :each, :js do |ex|
+      ex.run_with_retry retry: 3
+    end
+  end
+
   config.around(:each, :caching) do |example|
     caching_store = Rails.cache
     Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
diff --git a/spec/support/api/issues_resolving_discussions_shared_examples.rb b/spec/support/api/issues_resolving_discussions_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d26d279363c12781008ceebf1ffbe226bbabc7e5
--- /dev/null
+++ b/spec/support/api/issues_resolving_discussions_shared_examples.rb
@@ -0,0 +1,15 @@
+shared_examples 'creating an issue resolving discussions through the API' do
+  it 'creates a new project issue' do
+    expect(response).to have_http_status(:created)
+  end
+
+  it 'resolves the discussions in a merge request' do
+    discussion.first_note.reload
+
+    expect(discussion.resolved?).to be(true)
+  end
+
+  it 'assigns a description to the issue mentioning the merge request' do
+    expect(json_response['description']).to include(merge_request.to_reference)
+  end
+end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index 210cd5817e068170729a24580be40195bd0b5754..16a3cf06be7b95752cb343c2c6cdf7375fc92b87 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -7,13 +7,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
 
   describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
     context 'with an unauthorized user' do
-      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), duration: '1w') }
 
       it_behaves_like 'an unauthorized API user'
     end
 
     it "sets the time estimate for #{issuable_name}" do
-      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
 
       expect(response).to have_http_status(200)
       expect(json_response['human_time_estimate']).to eq('1w')
@@ -21,12 +21,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
 
     describe 'updating the current estimate' do
       before do
-        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
       end
 
       context 'when duration has a bad format' do
         it 'does not modify the original estimate' do
-          post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+          post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: 'foo'
 
           expect(response).to have_http_status(400)
           expect(issuable.reload.human_time_estimate).to eq('1w')
@@ -35,7 +35,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
 
       context 'with a valid duration' do
         it 'updates the estimate' do
-          post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+          post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '3w1h'
 
           expect(response).to have_http_status(200)
           expect(issuable.reload.human_time_estimate).to eq('3w 1h')
@@ -46,13 +46,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
 
   describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
     context 'with an unauthorized user' do
-      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) }
 
       it_behaves_like 'an unauthorized API user'
     end
 
     it "resets the time estimate for #{issuable_name}" do
-      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['time_estimate']).to eq(0)
@@ -62,7 +62,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
   describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
     context 'with an unauthorized user' do
       subject do
-        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", non_member),
              duration: '2h'
       end
 
@@ -70,7 +70,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
     end
 
     it "add spent time for #{issuable_name}" do
-      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
            duration: '2h'
 
       expect(response).to have_http_status(201)
@@ -81,7 +81,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
       it 'subtracts time of the total spent time' do
         issuable.update_attributes!(spend_time: { duration: 7200, user: user })
 
-        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
              duration: '-1h'
 
         expect(response).to have_http_status(201)
@@ -93,7 +93,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
       it 'does not modify the total time spent' do
         issuable.update_attributes!(spend_time: { duration: 7200, user: user })
 
-        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+        post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
              duration: '-1w'
 
         expect(response).to have_http_status(400)
@@ -104,13 +104,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
 
   describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
     context 'with an unauthorized user' do
-      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+      subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) }
 
       it_behaves_like 'an unauthorized API user'
     end
 
     it "resets spent time for #{issuable_name}" do
-      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+      post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['total_time_spent']).to eq(0)
@@ -122,7 +122,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
       issuable.update_attributes!(spend_time: { duration: 1800, user: user },
                                   time_estimate: 3600)
 
-      get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+      get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
 
       expect(response).to have_http_status(200)
       expect(json_response['total_time_spent']).to eq(1800)
diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f982b10d999500d36121b9a8278d05debd25a1fc
--- /dev/null
+++ b/spec/support/api/v3/time_tracking_shared_examples.rb
@@ -0,0 +1,128 @@
+shared_examples 'V3 time tracking endpoints' do |issuable_name|
+  issuable_collection_name = issuable_name.pluralize
+
+  describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+    context 'with an unauthorized user' do
+      subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+
+      it_behaves_like 'an unauthorized API user'
+    end
+
+    it "sets the time estimate for #{issuable_name}" do
+      post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+
+      expect(response).to have_http_status(200)
+      expect(json_response['human_time_estimate']).to eq('1w')
+    end
+
+    describe 'updating the current estimate' do
+      before do
+        post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+      end
+
+      context 'when duration has a bad format' do
+        it 'does not modify the original estimate' do
+          post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+
+          expect(response).to have_http_status(400)
+          expect(issuable.reload.human_time_estimate).to eq('1w')
+        end
+      end
+
+      context 'with a valid duration' do
+        it 'updates the estimate' do
+          post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+
+          expect(response).to have_http_status(200)
+          expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+        end
+      end
+    end
+  end
+
+  describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
+    context 'with an unauthorized user' do
+      subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+
+      it_behaves_like 'an unauthorized API user'
+    end
+
+    it "resets the time estimate for #{issuable_name}" do
+      post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['time_estimate']).to eq(0)
+    end
+  end
+
+  describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
+    context 'with an unauthorized user' do
+      subject do
+        post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+             duration: '2h'
+      end
+
+      it_behaves_like 'an unauthorized API user'
+    end
+
+    it "add spent time for #{issuable_name}" do
+      post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+           duration: '2h'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['human_total_time_spent']).to eq('2h')
+    end
+
+    context 'when subtracting time' do
+      it 'subtracts time of the total spent time' do
+        issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+        post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+             duration: '-1h'
+
+        expect(response).to have_http_status(201)
+        expect(json_response['total_time_spent']).to eq(3600)
+      end
+    end
+
+    context 'when time to subtract is greater than the total spent time' do
+      it 'does not modify the total time spent' do
+        issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+        post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+             duration: '-1w'
+
+        expect(response).to have_http_status(400)
+        expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
+      end
+    end
+  end
+
+  describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
+    context 'with an unauthorized user' do
+      subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+
+      it_behaves_like 'an unauthorized API user'
+    end
+
+    it "resets spent time for #{issuable_name}" do
+      post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['total_time_spent']).to eq(0)
+    end
+  end
+
+  describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
+    it "returns the time stats for #{issuable_name}" do
+      issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+                                  time_estimate: 3600)
+
+      get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['total_time_spent']).to eq(1800)
+      expect(json_response['time_estimate']).to eq(3600)
+    end
+  end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index ae6e708cf87b436474072db6a0ce74bd47615e5a..35d1e1cfc7dbb95c4b687b7d07feb9ee288716bb 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -49,8 +49,4 @@ module ApiHelpers
         ''
       end
   end
-
-  def json_response
-    @_json_response ||= JSON.parse(response.body)
-  end
 end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 16d5f2bf0b8421eac464307fe631fe240de97278..aa14709bc9c57440e7de621caf252cae7538966c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,9 +1,10 @@
 require 'capybara/rails'
 require 'capybara/rspec'
 require 'capybara/poltergeist'
+require 'capybara-screenshot/rspec'
 
 # Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
 
 Capybara.javascript_driver = :poltergeist
 Capybara.register_driver :poltergeist do |app|
@@ -21,12 +22,8 @@ end
 Capybara.default_max_wait_time = timeout
 Capybara.ignore_hidden_elements = true
 
-unless ENV['CI'] || ENV['CI_SERVER']
-  require 'capybara-screenshot/rspec'
-
-  # Keep only the screenshots generated from the last failing test suite
-  Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
 
 RSpec.configure do |config|
   config.before(:suite) do
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
index 72af2c70324ca10db0d7acf5a568d5299ea7155d..b4b016e408f7fddfa671bc08fe15f6de63a0b84e 100644
--- a/spec/support/carrierwave.rb
+++ b/spec/support/carrierwave.rb
@@ -1,7 +1,7 @@
-CarrierWave.root = 'tmp/tests/uploads'
+CarrierWave.root = File.expand_path('tmp/tests/public', Rails.root)
 
 RSpec.configure do |config|
   config.after(:each) do
-    FileUtils.rm_rf('tmp/tests/uploads')
+    FileUtils.rm_rf(CarrierWave.root)
   end
 end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 6ed55289ed9ee97ac2c722d509b31f5b0d49079a..c864a705ca4034b62e051cb8479516b9300dfbc5 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -9,14 +9,7 @@ module CycleAnalyticsHelpers
     commit_shas = Array.new(count) do |index|
       filename = random_git_name
 
-      options = {
-        committer: project.repository.user_to_committer(user),
-        author: project.repository.user_to_committer(user),
-        commit: { message: message, branch: branch_name, update_ref: true },
-        file: { content: "content", path: filename, update: false }
-      }
-
-      commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+      commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name)
       project.repository.commit(commit_sha)
 
       commit_sha
@@ -35,13 +28,12 @@ module CycleAnalyticsHelpers
       project.repository.add_branch(user, source_branch, 'master')
     end
 
-    sha = project.repository.commit_file(
+    sha = project.repository.create_file(
       user,
       random_git_name,
       'content',
       message: 'commit message',
-      branch_name: source_branch,
-      update: false)
+      branch_name: source_branch)
     project.repository.commit(sha)
 
     opts = {
diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..984ec7d2741dc0fc2cf07a50f54f8c8e6be35e42
--- /dev/null
+++ b/spec/support/dropzone_helper.rb
@@ -0,0 +1,37 @@
+module DropzoneHelper
+  # Provides a way to perform `attach_file` for a Dropzone-based file input
+  #
+  # This is accomplished by creating a standard HTML file input on the page,
+  # performing `attach_file` on that field, and then triggering the appropriate
+  # Dropzone events to perform the actual upload.
+  #
+  # This method waits for the upload to complete before returning.
+  def dropzone_file(file_path)
+    # Generate a fake file input that Capybara can attach to
+    page.execute_script <<-JS.strip_heredoc
+      var fakeFileInput = window.$('<input/>').attr(
+        {id: 'fakeFileInput', type: 'file'}
+      ).appendTo('body');
+
+      window._dropzoneComplete = false;
+    JS
+
+    # Attach the file to the fake input selector with Capybara
+    attach_file('fakeFileInput', file_path)
+
+    # Manually trigger a Dropzone "drop" event with the fake input's file list
+    page.execute_script <<-JS.strip_heredoc
+      var fileList = [$('#fakeFileInput')[0].files[0]];
+      var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+
+      var dropzone = $('.div-dropzone')[0].dropzone;
+      dropzone.on('queuecomplete', function() {
+        window._dropzoneComplete = true;
+      });
+      dropzone.listeners[0].events.drop(e);
+    JS
+
+    # Wait until Dropzone's fired `queuecomplete`
+    loop until page.evaluate_script('window._dropzoneComplete === true')
+  end
+end
diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a946995f8427fca45e9c5e83ee991634c892c24
--- /dev/null
+++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
@@ -0,0 +1,41 @@
+shared_examples 'creating an issue for a discussion' do
+  it 'shows an issue with the title filled in' do
+    title_field = page.find_field('issue[title]')
+
+    expect(title_field.value).to include(merge_request.title)
+  end
+
+  it 'has a mention of the discussion in the description'  do
+    description_field = page.find_field('issue[description]')
+
+    expect(description_field.value).to include(discussion.first_note.note)
+  end
+
+  it 'can create a new issue for the project' do
+    expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
+  end
+
+  it 'resolves the discussion in the merge request' do
+    click_button 'Submit issue'
+
+    discussion.first_note.reload
+
+    expect(discussion.resolved?).to eq(true)
+  end
+
+  it 'shows a flash messaage after resolving a discussion' do
+    click_button 'Submit issue'
+
+    page.within '.flash-notice' do
+      # Only check for the word 'Resolved' since the spec might have resolved
+      # multiple discussions
+      expect(page).to have_content('Resolved')
+    end
+  end
+
+  it 'has a hidden field for the merge request' do
+    merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false)
+
+    expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+  end
+end
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a3b0a731ad5f97badc16449faf324a7c238b542
--- /dev/null
+++ b/spec/support/features/rss_shared_examples.rb
@@ -0,0 +1,23 @@
+shared_examples "an autodiscoverable RSS feed with current_user's private token" do
+  it "has an RSS autodiscovery link tag with current_user's private token" do
+    expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false)
+  end
+end
+
+shared_examples "it has an RSS button with current_user's private token" do
+  it "shows the RSS button with current_user's private token" do
+    expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']")
+  end
+end
+
+shared_examples "an autodiscoverable RSS feed without a private token" do
+  it "has an RSS autodiscovery link tag without a private token" do
+    expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false)
+  end
+end
+
+shared_examples "it has an RSS button without a private token" do
+  it "shows the RSS button without a private token" do
+    expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])")
+  end
+end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index 58f6636e680297fde32c00a2331b9af9b906285b..6b009b132b6cc4436e6ba7521927dcea34afe1a5 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -3,16 +3,25 @@ module FilteredSearchHelpers
     page.find('.filtered-search')
   end
 
-  def input_filtered_search(search_term, submit: true)
-    filtered_search.set(search_term)
+  # Enables input to be set (similar to copy and paste)
+  def input_filtered_search(search_term, submit: true, extra_space: true)
+    search = search_term
+    if extra_space
+      # Add an extra space to engage visual tokens
+      search = "#{search_term} "
+    end
+
+    filtered_search.set(search)
 
     if submit
       filtered_search.send_keys(:enter)
     end
   end
 
+  # Enables input to be added character by character
   def input_filtered_search_keys(search_term)
-    filtered_search.send_keys(search_term)
+    # Add an extra space to engage visual tokens
+    filtered_search.send_keys("#{search_term} ")
     filtered_search.send_keys(:enter)
   end
 
@@ -34,4 +43,32 @@ module FilteredSearchHelpers
     # This ensures the dropdown is shown
     expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
   end
+
+  def expect_filtered_search_input_empty
+    expect(find('.filtered-search').value).to eq('')
+  end
+
+  # Iterates through each visual token inside
+  # .tokens-container to make sure the correct names and values are rendered
+  def expect_tokens(tokens)
+    page.find '.filtered-search-input-container .tokens-container' do
+      page.all(:css, '.tokens-container li').each_with_index do |el, index|
+        token_name = tokens[index][:name]
+        token_value = tokens[index][:value]
+
+        expect(el.find('.name')).to have_content(token_name)
+        if token_value
+          expect(el.find('.value')).to have_content(token_value)
+        end
+      end
+    end
+  end
+
+  def default_placeholder
+    'Search or filter results...'
+  end
+
+  def get_filtered_search_placeholder
+    find('.filtered-search')['placeholder']
+  end
 end
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
index dac94dfc31e3582780f4dc0e29c095282bea2193..4c0f556e73631c33bc71e20d285a1a041a7b1515 100644
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -3,11 +3,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
     @issuable_ids = []
 
     2.times do
-      if issuable_type == :issue
-        issuable = create(issuable_type, project: project)
-      else
-        issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
-      end
+      issuable =
+        if issuable_type == :issue
+          create(issuable_type, project: project)
+        else
+          create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+        end
 
       @issuable_ids << issuable.id
 
@@ -21,7 +22,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
     if action
       get action
     else
-      get :index, namespace_id: project.namespace.path, project_id: project.path
+      get :index, namespace_id: project.namespace, project_id: project
     end
 
     meta_data = assigns(:issuable_meta_data)
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 0b8729db0f9e8a3fab4b4a839c7ba5e442398e0b..a982b159b488f22e54ac984ca74dfc7a31797e99 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -5,7 +5,7 @@ require 'gitlab/popen'
 module JavaScriptFixturesHelpers
   include Gitlab::Popen
 
-  FIXTURE_PATH = 'spec/javascripts/fixtures'
+  FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
 
   # Public: Removes all fixture files from given directory
   #
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 929fc0c5182e43d838ff1643d3721a5926bde2d5..97ae0b6afc51bc281c846fc7f7c08e13baa552ff 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,5 +1,5 @@
 module JiraServiceHelper
-  JIRA_URL = "http://jira.example.net"
+  JIRA_URL = "http://jira.example.net".freeze
   JIRA_API = JIRA_URL + "/rest/api/2"
 
   def jira_service_settings
diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e8d2ef2d7f07b1dd2ddd7926e8d3b815f94cf30c
--- /dev/null
+++ b/spec/support/json_response_helpers.rb
@@ -0,0 +1,9 @@
+shared_context 'JSON response' do
+  let(:json_response) { JSON.parse(response.body) }
+end
+
+RSpec.configure do |config|
+  config.include_context 'JSON response', type: :controller
+  config.include_context 'JSON response', type: :request
+  config.include_context 'JSON response', :api
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index 444612cf8713091a75762c2a0e83fbb980478aa3..b5ed71ba3be162af4c258e8c98f76b7d5dbd59b1 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -2,23 +2,24 @@ module KubernetesHelpers
   include Gitlab::Kubernetes
 
   def kube_discovery_body
-    { "kind" => "APIResourceList",
+    {
+      "kind" => "APIResourceList",
       "resources" => [
         { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
-      ],
+      ]
     }
   end
 
   def kube_pods_body(*pods)
     { "kind" => "PodList",
-      "items" => [ kube_pod ],
-    }
+      "items" => [kube_pod] }
   end
 
   # This is a partial response, it will have many more elements in reality but
   # these are the ones we care about at the moment
   def kube_pod(app: "valid-pod-label")
-    { "metadata" => {
+    {
+      "metadata" => {
         "name" => "kube-pod",
         "creationTimestamp" => "2016-11-25T19:55:19Z",
         "labels" => { "app" => app },
@@ -29,7 +30,7 @@ module KubernetesHelpers
           { "name" => "container-1" },
         ],
       },
-      "status" => { "phase" => "Running" },
+      "status" => { "phase" => "Running" }
     }
   end
 
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ad1eed5b369de544dde7af72a6b3c14924a4a8a2..9ffb00be0b8f3212f22c5a15dd2d59e7b69e30b4 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -15,11 +15,12 @@ module LoginHelpers
   #   user = create(:user)
   #   login_as(user)
   def login_as(user_or_role)
-    if user_or_role.kind_of?(User)
-      @user = user_or_role
-    else
-      @user = create(user_or_role)
-    end
+    @user =
+      if user_or_role.is_a?(User)
+        user_or_role
+      else
+        create(user_or_role)
+      end
 
     login_with(@user)
   end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index a79386b5db9618dc1eeb52ee016ad124fa39c3a5..dea0015f105fdd82be6f9a0cb672b216801f0808 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -79,8 +79,8 @@ class MarkdownFeature
 
   def xproject
     @xproject ||= begin
-      namespace = create(:namespace, name: 'cross-reference')
-      create(:project, namespace: namespace) do |project|
+      group = create(:group, :nested)
+      create(:project, namespace: group) do |project|
         project.team << [user, :developer]
       end
     end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index ceddb6565961a0d5aa67c5bd65c552828f6016ce..7d238850520d043ca53d1a495e8238b5da33d80f 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -38,7 +38,7 @@ module AccessMatchers
   end
 
   def description_for(user, type)
-    if user.kind_of?(User)
+    if user.is_a?(User)
       # User#inspect displays too much information for RSpec's descriptions
       "be #{type} for the specified user"
     else
diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9d59ec12ec20583acb4535f69004feb3b586b30
--- /dev/null
+++ b/spec/support/matchers/email_matchers.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_html_escaped_body_text do |expected|
+  match do |actual|
+    expect(actual).to have_body_text(ERB::Util.html_escape(expected))
+  end
+end
diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d7a538206845f2b759fdb6e7f010baf9fcc7a425
--- /dev/null
+++ b/spec/support/matchers/gitaly_matchers.rb
@@ -0,0 +1,3 @@
+RSpec::Matchers.define :post_receive_request_with_repo_path do |path|
+  match { |actual| actual.repository.path == path }
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 97b8b342eb22a46bed8383a62213a8880c6269f1..bbbbaf4c5e88cb2a694759d331d7a555243dd5be 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -26,10 +26,11 @@ module MarkdownMatchers
     set_default_markdown_messages
 
     match do |actual|
-      expect(actual).to have_selector('img.emoji', count: 10)
+      expect(actual).to have_selector('gl-emoji', count: 10)
 
-      image = actual.at_css('img.emoji')
-      expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets')
+      emoji_element = actual.at_css('gl-emoji')
+      expect(emoji_element['data-name'].to_s).not_to be_empty
+      expect(emoji_element['data-unicode-version'].to_s).not_to be_empty
     end
   end
 
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb
index 4621d17549b9f3badc42e06235644f6628c046fd..f8b7d0527ba5f7dd459fc87b70fb621aa24aca89 100644
--- a/spec/support/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass|
 
   before do
     _ = issuable
-    login_as(user)
+    login_as(user) if user
     visit path
   end
 
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a52d8f37d144654a4a7393a052eb8cac992c716e
--- /dev/null
+++ b/spec/support/prometheus_helpers.rb
@@ -0,0 +1,117 @@
+module PrometheusHelpers
+  def prometheus_memory_query(environment_slug)
+    %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
+  end
+
+  def prometheus_cpu_query(environment_slug)
+    %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+  end
+
+  def prometheus_query_url(prometheus_query)
+    query = { query: prometheus_query }.to_query
+
+    "https://prometheus.example.com/api/v1/query?#{query}"
+  end
+
+  def prometheus_query_range_url(prometheus_query, start: 8.hours.ago)
+    query = {
+      query: prometheus_query,
+      start: start.to_f,
+      end: Time.now.utc.to_f,
+      step: 1.minute.to_i
+    }.to_query
+
+    "https://prometheus.example.com/api/v1/query_range?#{query}"
+  end
+
+  def stub_prometheus_request(url, body: {}, status: 200)
+    WebMock.stub_request(:get, url)
+      .to_return({
+        status: status,
+        headers: { 'Content-Type' => 'application/json' },
+        body: body.to_json
+      })
+  end
+
+  def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
+    stub_prometheus_request(
+      prometheus_query_url(prometheus_memory_query(environment_slug)),
+      status: status,
+      body: body || prometheus_value_body
+    )
+    stub_prometheus_request(
+      prometheus_query_range_url(prometheus_memory_query(environment_slug)),
+      status: status,
+      body: body || prometheus_values_body
+    )
+    stub_prometheus_request(
+      prometheus_query_url(prometheus_cpu_query(environment_slug)),
+      status: status,
+      body: body || prometheus_value_body
+    )
+    stub_prometheus_request(
+      prometheus_query_range_url(prometheus_cpu_query(environment_slug)),
+      status: status,
+      body: body || prometheus_values_body
+    )
+  end
+
+  def prometheus_data(last_update: Time.now.utc)
+    {
+      success: true,
+      metrics: {
+        memory_values: prometheus_values_body('matrix').dig(:data, :result),
+        memory_current: prometheus_value_body('vector').dig(:data, :result),
+        cpu_values: prometheus_values_body('matrix').dig(:data, :result),
+        cpu_current: prometheus_value_body('vector').dig(:data, :result)
+      },
+      last_update: last_update
+    }
+  end
+
+  def prometheus_empty_body(type)
+    {
+      "status": "success",
+      "data": {
+        "resultType": type,
+        "result": []
+      }
+    }
+  end
+
+  def prometheus_value_body(type = 'vector')
+    {
+      "status": "success",
+      "data": {
+        "resultType": type,
+        "result": [
+          {
+            "metric": {},
+            "value": [
+              1488772511.004,
+              "0.000041021495238095323"
+            ]
+          }
+        ]
+      }
+    }
+  end
+
+  def prometheus_values_body(type = 'matrix')
+    {
+      "status": "success",
+      "data": {
+        "resultType": type,
+        "result": [
+          {
+            "metric": {},
+            "values": [
+              [1488758662.506, "0.00002996364761904785"],
+              [1488758722.506, "0.00003090239047619091"]
+            ]
+          }
+        ]
+      }
+    }
+  end
+end
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index 73f375c481bf9ed5b7af6745f90828d9afbdb4bb..e9d5c7b12ae62cb6fd1e82dafbca99944c28f124 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
 eos
     )
   end
-  
+
   def another_sample_commit
     OpenStruct.new(
       id: "e56497bb5f03a90a51293fc6d516788730953899",
@@ -100,13 +100,13 @@ eos
       }
     ]
 
-    commits = [
-      '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
-      '570e7b2abdd848b95f2f578043fc23bd6f6fd24d',
-      '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
-      'd14d6c0abdd253381df51a723d58691b2ee1ab08',
-      'c1acaa58bbcbc3eafe538cb8274ba387047b69f8',
-    ].reverse # last commit is recent one
+    commits = %w(
+      5937ac0a7beb003549fc5fd26fc247adbce4a52e
+      570e7b2abdd848b95f2f578043fc23bd6f6fd24d
+      6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+      d14d6c0abdd253381df51a723d58691b2ee1ab08
+      c1acaa58bbcbc3eafe538cb8274ba387047b69f8
+    ).reverse # last commit is recent one
 
     OpenStruct.new(
       source_branch: 'master',
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 03fa0a66b9abfdf6a3779a1e89df763f52d02715..f55fee28ff938093420372b5cb978b0054aa4f2f 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
 TEST_BROKEN_REPO_PATH  = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
 
 module SeedHelper
-  GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
+  GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze
 
   def ensure_seeds
     if File.exist?(SEED_REPOSITORY_PATH)
@@ -25,7 +25,7 @@ module SeedHelper
   end
 
   def create_bare_seeds
-    system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
+    system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
            chdir: SEED_REPOSITORY_PATH,
            out:   '/dev/null',
            err:   '/dev/null')
@@ -45,7 +45,7 @@ module SeedHelper
     system(git_env, *%w(git branch -t feature origin/feature),
            chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
 
-    system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
+    system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
            chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
   end
 
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
index 9f2cd7c67c5042faa503762717e8d25e24e7fe5a..99a500bbbb1b5faa5bf26c5fc34dfedacbe7d854 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/seed_repo.rb
@@ -25,64 +25,64 @@
 
 module SeedRepo
   module BigCommit
-    ID               = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
-    PARENT_ID        = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
-    MESSAGE          = "Files, encoding and much more"
-    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+    ID               = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze
+    PARENT_ID        = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze
+    MESSAGE          = "Files, encoding and much more".freeze
+    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
     FILES_COUNT      = 2
   end
 
   module Commit
-    ID               = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
-    PARENT_ID        = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
-    MESSAGE          = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
-    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
-    FILES            = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
+    ID               = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze
+    PARENT_ID        = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze
+    MESSAGE          = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze
+    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+    FILES            = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze
     FILES_COUNT      = 2
-    C_FILE_PATH      = "files/ruby"
-    C_FILES          = ["popen.rb", "regex.rb", "version_info.rb"]
-    BLOB_FILE        = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n  = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
-    BLOB_FILE_PATH   = "app/views/keys/show.html.haml"
+    C_FILE_PATH      = "files/ruby".freeze
+    C_FILES          = ["popen.rb", "regex.rb", "version_info.rb"].freeze
+    BLOB_FILE        = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n  = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}.freeze
+    BLOB_FILE_PATH   = "app/views/keys/show.html.haml".freeze
   end
 
   module EmptyCommit
-    ID               = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
-    PARENT_ID        = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
-    MESSAGE          = "Empty commit"
-    AUTHOR_FULL_NAME = "Rémy Coutable"
-    FILES            = []
+    ID               = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze
+    PARENT_ID        = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+    MESSAGE          = "Empty commit".freeze
+    AUTHOR_FULL_NAME = "Rémy Coutable".freeze
+    FILES            = [].freeze
     FILES_COUNT      = FILES.count
   end
 
   module EncodingCommit
-    ID               = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
-    PARENT_ID        = "66028349a123e695b589e09a36634d976edcc5e8"
-    MESSAGE          = "Add ISO-8859-encoded file"
-    AUTHOR_FULL_NAME = "Stan Hu"
-    FILES            = ["encoding/iso8859.txt"]
+    ID               = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+    PARENT_ID        = "66028349a123e695b589e09a36634d976edcc5e8".freeze
+    MESSAGE          = "Add ISO-8859-encoded file".freeze
+    AUTHOR_FULL_NAME = "Stan Hu".freeze
+    FILES            = ["encoding/iso8859.txt"].freeze
     FILES_COUNT      = FILES.count
   end
 
   module FirstCommit
-    ID               = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
+    ID               = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze
     PARENT_ID        = nil
-    MESSAGE          = "Initial commit"
-    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
-    FILES            = ["LICENSE", ".gitignore", "README.md"]
+    MESSAGE          = "Initial commit".freeze
+    AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+    FILES            = ["LICENSE", ".gitignore", "README.md"].freeze
     FILES_COUNT      = 3
   end
 
   module LastCommit
-    ID               = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
-    PARENT_ID        = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
-    MESSAGE          = "Merge branch 'master' into 'master'"
-    AUTHOR_FULL_NAME = "Stan Hu"
-    FILES            = ["bin/executable"]
+    ID               = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6".freeze
+    PARENT_ID        = "0e1b353b348f8477bdbec1ef47087171c5032cd9".freeze
+    MESSAGE          = "Merge branch 'master' into 'master'".freeze
+    AUTHOR_FULL_NAME = "Stan Hu".freeze
+    FILES            = ["bin/executable"].freeze
     FILES_COUNT      = FILES.count
   end
 
   module Repo
-    HEAD = "master"
+    HEAD = "master".freeze
     BRANCHES = %w[
       feature
       fix
@@ -93,14 +93,14 @@ module SeedRepo
       gitattributes-updated
       master
       merge-test
-    ]
-    TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
+    ].freeze
+    TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1].freeze
   end
 
   module RubyBlob
-    ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
-    NAME = "popen.rb"
-    CONTENT = <<-eos
+    ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
+    NAME = "popen.rb".freeze
+    CONTENT = <<-eos.freeze
 require 'fileutils'
 require 'open3'
 
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index d30cc8ff9f2955f8525559deab6d3020fffd3de1..0d526045012f206d802c830c1a434580d9320330 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -12,7 +12,7 @@
 
 module Select2Helper
   def select2(value, options = {})
-    raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
+    raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
 
     selector = options.fetch(:from)
 
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index c64574679b6debd94f0ccab013bf5ae8b191a409..81d06dc2a3dd27372af27d7cf930c6278f17e63a 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -11,7 +11,7 @@ shared_examples 'new issuable record that supports slash commands' do
   let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
   let(:issuable) { described_class.new(project, user, params).execute }
 
-  before { project.team << [assignee, :master ] }
+  before { project.team << [assignee, :master] }
 
   context 'with labels in command only' do
     let(:example_params) do
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 93f96cacc00178f72768284f6d99fdb577708a8e..a01ef5762341dac3f47c2bffe9edc99ae3503868 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -35,7 +35,7 @@ module StubGitlabCalls
       { "tags" => tags }
     )
     allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
-      JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
+      JSON.parse(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
     )
     allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
       File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index b87232a350b33f137c16c94586f223e1a972c65e..f1d226b6ae31384418970feec7daf99d70d527c8 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -38,7 +38,7 @@ module TestEnv
     'deleted-image-test'                 => '6c17798',
     'wip'                                => 'b9238ee',
     'csv'                                => '3dd0896'
-  }
+  }.freeze
 
   # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
   # need to keep all the branches in sync.
@@ -48,7 +48,7 @@ module TestEnv
     'master'                     => '5937ac0',
     'remove-submodule'           => '2a33e0c',
     'conflict-resolvable-fork'   => '404fa3f'
-  }
+  }.freeze
 
   # Test environment
   #
@@ -135,7 +135,7 @@ module TestEnv
 
   def copy_repo(project)
     base_repo_path = File.expand_path(factory_repo_path_bare)
-    target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+    target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
     FileUtils.mkdir_p(target_repo_path)
     FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
     FileUtils.chmod_R 0755, target_repo_path
@@ -143,7 +143,7 @@ module TestEnv
   end
 
   def repos_path
-    Gitlab.config.repositories.storages.default
+    Gitlab.config.repositories.storages.default['path']
   end
 
   def backup_path
@@ -152,7 +152,7 @@ module TestEnv
 
   def copy_forked_repo_with_submodules(project)
     base_repo_path = File.expand_path(forked_repo_path_bare)
-    target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+    target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
     FileUtils.mkdir_p(target_repo_path)
     FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
     FileUtils.chmod_R 0755, target_repo_path
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7cf5a65eeedcdc9a58c3e6db10ee673eb610909d
--- /dev/null
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -0,0 +1,62 @@
+shared_context 'unique ips sign in limit' do
+  include StubENV
+  before(:each) do
+    Gitlab::Redis.with(&:flushall)
+  end
+
+  before do
+    stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+    current_application_settings.update!(
+      unique_ips_limit_enabled: true,
+      unique_ips_limit_time_window: 10000
+    )
+  end
+
+  def change_ip(ip)
+    allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip)
+  end
+
+  def request_from_ip(ip)
+    change_ip(ip)
+    request
+    response
+  end
+
+  def operation_from_ip(ip)
+    change_ip(ip)
+    operation
+  end
+end
+
+shared_examples 'user login operation with unique ip limit' do
+  include_context 'unique ips sign in limit' do
+    before { current_application_settings.update!(unique_ips_limit_per_user: 1) }
+
+    it 'allows user authenticating from the same ip' do
+      expect { operation_from_ip('ip') }.not_to raise_error
+      expect { operation_from_ip('ip') }.not_to raise_error
+    end
+
+    it 'blocks user authenticating from two distinct ips' do
+      expect { operation_from_ip('ip') }.not_to raise_error
+      expect { operation_from_ip('ip2') }.to raise_error(Gitlab::Auth::TooManyIps)
+    end
+  end
+end
+
+shared_examples 'user login request with unique ip limit' do |success_status = 200|
+  include_context 'unique ips sign in limit' do
+    before { current_application_settings.update!(unique_ips_limit_per_user: 1) }
+
+    it 'allows user authenticating from the same ip' do
+      expect(request_from_ip('ip')).to have_http_status(success_status)
+      expect(request_from_ip('ip')).to have_http_status(success_status)
+    end
+
+    it 'blocks user authenticating from two distinct ips' do
+      expect(request_from_ip('ip')).to have_http_status(success_status)
+      expect(request_from_ip('ip2')).to have_http_status(403)
+    end
+  end
+end
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..365c34448ac700be0c627661f32077ba479951ea
--- /dev/null
+++ b/spec/support/update_invalid_issuable.rb
@@ -0,0 +1,57 @@
+shared_examples 'update invalid issuable' do |klass|
+  let(:params) do
+    {
+      namespace_id: project.namespace.path,
+      project_id: project.path,
+      id: issuable.iid
+    }
+  end
+
+  let(:issuable) do
+    klass == Issue ? issue : merge_request
+  end
+
+  before do
+    if klass == Issue
+      params.merge!(issue: { title: "any" })
+    else
+      params.merge!(merge_request: { title: "any" })
+    end
+  end
+
+  context 'when updating causes conflicts' do
+    before do
+      allow_any_instance_of(issuable.class).to receive(:save).
+        and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
+    end
+
+    it 'renders edit when format is html' do
+      put :update, params
+
+      expect(response).to render_template(:edit)
+      expect(assigns[:conflict]).to be_truthy
+    end
+
+    it 'renders json error message when format is json' do
+      params[:format] = "json"
+
+      put :update, params
+
+      expect(response.status).to eq(409)
+      expect(JSON.parse(response.body)).to have_key('errors')
+    end
+  end
+
+  context 'when updating an invalid issuable' do
+    before do
+      key = klass == Issue ? :issue : :merge_request
+      params[key][:title] = ""
+    end
+
+    it 'renders edit when merge request is invalid' do
+      put :update, params
+
+      expect(response).to render_template(:edit)
+    end
+  end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index df8a47893f9034935a6ba159ec8852c4cbc4619a..10458966cb955cdb903da4b83ea68340e06130ae 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -108,7 +108,7 @@ describe 'gitlab:app namespace rake task' do
       $stdout = orig_stdout
     end
 
-    describe 'backup creation and deletion using annex and custom_hooks' do
+    describe 'backup creation and deletion using custom_hooks' do
       let(:project) { create(:project) }
       let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
 
@@ -132,25 +132,6 @@ describe 'gitlab:app namespace rake task' do
         Dir.chdir(@origin_cd)
       end
 
-      context 'project uses git-annex and successfully creates backup' do
-        let(:filename) { "annex" }
-
-        it 'creates annex.tar and project bundle' do
-          tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
-
-          expect(exit_status).to eq(0)
-          expect(tar_contents).to match(user_backup_path)
-          expect(tar_contents).to match("#{user_backup_path}/annex.tar")
-          expect(tar_contents).to match("#{user_backup_path}.bundle")
-        end
-
-        it 'restores files correctly' do
-          restore_backup
-
-          expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt")
-        end
-      end
-
       context 'project uses custom_hooks and successfully creates backup' do
         let(:filename) { "custom_hooks" }
 
@@ -246,8 +227,8 @@ describe 'gitlab:app namespace rake task' do
         FileUtils.mkdir('tmp/tests/default_storage')
         FileUtils.mkdir('tmp/tests/custom_storage')
         storages = {
-          'default' => 'tmp/tests/default_storage',
-          'custom' => 'tmp/tests/custom_storage'
+          'default' => { 'path' => 'tmp/tests/default_storage' },
+          'custom' => { 'path' => 'tmp/tests/custom_storage' }
         }
         allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
 
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca74378a12a5a545bcc0066bff388b4ae3f0c920
--- /dev/null
+++ b/spec/tasks/gitlab/info_rake_spec.rb
@@ -0,0 +1,37 @@
+require 'rake_helper'
+
+describe 'gitlab:env:info' do
+  before do
+    Rake.application.rake_require 'tasks/gitlab/info'
+
+    stub_warn_user_is_not_gitlab
+    allow(Gitlab::Popen).to receive(:popen)
+  end
+
+  describe 'git version' do
+    before do
+      allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version'])
+        .and_return(git_version)
+    end
+
+    context 'when git installed' do
+      let(:git_version) { 'git version 2.10.0' }
+
+      it 'prints git version' do
+        run_rake_task('gitlab:env:info')
+
+        expect($stdout.string).to match(/Git Version:(.*)2.10.0/)
+      end
+    end
+
+    context 'when git not installed' do
+      let(:git_version) { '' }
+
+      it 'prints unknown' do
+        run_rake_task('gitlab:env:info')
+
+        expect($stdout.string).to match(/Git Version:(.*)unknown/)
+      end
+    end
+  end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 6098be5cd45508848298b28c4016fd0ccb74b8aa..ea714fb08f0157c946f2c9d9480a9b9d07ff2b6e 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,18 +1,17 @@
 require 'spec_helper'
 
 describe AttachmentUploader do
-  let(:issue) { build(:issue) }
-  subject { described_class.new(issue) }
+  let(:uploader) { described_class.new(build_stubbed(:user)) }
 
   describe '#move_to_cache' do
     it 'is true' do
-      expect(subject.move_to_cache).to eq(true)
+      expect(uploader.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)
+      expect(uploader.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
index 76f5a4b42ed024dd754c2f2e9046df3845cdda18..c4d558805ab2983cbd055f7f94d9f23e789add55 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,18 +1,17 @@
 require 'spec_helper'
 
 describe AvatarUploader do
-  let(:user) { build(:user) }
-  subject { described_class.new(user) }
+  let(:uploader) { described_class.new(build_stubbed(:user)) }
 
   describe '#move_to_cache' do
     it 'is false' do
-      expect(subject.move_to_cache).to eq(false)
+      expect(uploader.move_to_cache).to eq(false)
     end
   end
 
   describe '#move_to_store' do
     it 'is false' do
-      expect(subject.move_to_store).to eq(false)
+      expect(uploader.move_to_store).to eq(false)
     end
   end
 end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 6a712e33c962caa6543d35f673b1a9adf45e5dfc..d9113ef4095d8bf3face70d97ec7360b6c0a8220 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,57 +1,56 @@
 require 'spec_helper'
 
 describe FileUploader do
-  let(:project) { create(:project) }
+  let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
 
-  before do
-    @previous_enable_processing = FileUploader.enable_processing
-    FileUploader.enable_processing = false
-    @uploader = FileUploader.new(project)
-  end
-
-  after do
-    FileUploader.enable_processing = @previous_enable_processing
-    @uploader.remove!
-  end
+  describe '.absolute_path' do
+    it 'returns the correct absolute path by building it dynamically' do
+      project = build_stubbed(:project)
+      upload = double(model: project, path: 'secret/foo.jpg')
 
-  describe '#image_or_video?' do
-    context 'given an image file' do
-      before do
-        @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
-      end
+      dynamic_segment = project.path_with_namespace
 
-      it 'detects an image based on file extension' do
-        expect(@uploader.image_or_video?).to be true
-      end
+      expect(described_class.absolute_path(upload))
+        .to end_with("#{dynamic_segment}/secret/foo.jpg")
     end
+  end
+
+  describe 'initialize' do
+    it 'generates a secret if none is provided' do
+      expect(SecureRandom).to receive(:hex).and_return('secret')
 
-    context 'given an video file' do
-      before do
-        video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
-        @uploader.store!(video_file)
-      end
+      uploader = described_class.new(double)
 
-      it 'detects a video based on file extension' do
-        expect(@uploader.image_or_video?).to be true
-      end
+      expect(uploader.secret).to eq 'secret'
     end
 
-    it 'does not return image_or_video? for other types' do
-      @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+    it 'accepts a secret parameter' do
+      expect(SecureRandom).not_to receive(:hex)
 
-      expect(@uploader.image_or_video?).to be false
+      uploader = described_class.new(double, 'secret')
+
+      expect(uploader.secret).to eq 'secret'
     end
   end
 
   describe '#move_to_cache' do
     it 'is true' do
-      expect(@uploader.move_to_cache).to eq(true)
+      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)
+      expect(uploader.move_to_store).to eq(true)
+    end
+  end
+
+  describe '#relative_path' do
+    it 'removes the leading dynamic path segment' do
+      fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+      uploader.store!(fixture_file_upload(fixture))
+
+      expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/)
     end
   end
 end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5c26e334a6e58ecd634856c56f65565cbbea0e82
--- /dev/null
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -0,0 +1,97 @@
+require 'rails_helper'
+
+describe RecordsUploads do
+  let(:uploader) do
+    class RecordsUploadsExampleUploader < GitlabUploader
+      include RecordsUploads
+
+      storage :file
+
+      def model
+        FactoryGirl.build_stubbed(:user)
+      end
+    end
+
+    RecordsUploadsExampleUploader.new
+  end
+
+  def upload_fixture(filename)
+    fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+  end
+
+  describe 'callbacks' do
+    it 'calls `record_upload` after `store`' do
+      expect(uploader).to receive(:record_upload).once
+
+      uploader.store!(upload_fixture('doc_sample.txt'))
+    end
+
+    it 'calls `destroy_upload` after `remove`' do
+      expect(uploader).to receive(:destroy_upload).once
+
+      uploader.store!(upload_fixture('doc_sample.txt'))
+
+      uploader.remove!
+    end
+  end
+
+  describe '#record_upload callback' do
+    it 'returns early when not using file storage' do
+      allow(uploader).to receive(:file_storage?).and_return(false)
+      expect(Upload).not_to receive(:record)
+
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+    end
+
+    it "returns early when the file doesn't exist" do
+      allow(uploader).to receive(:file).and_return(double(exists?: false))
+      expect(Upload).not_to receive(:record)
+
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+    end
+
+    it 'creates an Upload record after store' do
+      expect(Upload).to receive(:record)
+        .with(uploader)
+
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+    end
+
+    it 'it destroys Upload records at the same path before recording' do
+      existing = Upload.create!(
+        path: File.join('uploads', 'rails_sample.jpg'),
+        size: 512.kilobytes,
+        model: build_stubbed(:user),
+        uploader: uploader.class.to_s
+      )
+
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+
+      expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      expect(Upload.count).to eq 1
+    end
+  end
+
+  describe '#destroy_upload callback' do
+    it 'returns early when not using file storage' do
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+
+      allow(uploader).to receive(:file_storage?).and_return(false)
+      expect(Upload).not_to receive(:remove_path)
+
+      uploader.remove!
+    end
+
+    it 'returns early when file is nil' do
+      expect(Upload).not_to receive(:remove_path)
+
+      uploader.remove!
+    end
+
+    it 'it destroys Upload records at the same path after removal' do
+      uploader.store!(upload_fixture('rails_sample.jpg'))
+
+      expect { uploader.remove! }.to change { Upload.count }.from(1).to(0)
+    end
+  end
+end
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c47f09adb6d792571229e5c61953f69b219e2b0c
--- /dev/null
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe UploaderHelper do
+  let(:uploader) do
+    example_uploader = Class.new(CarrierWave::Uploader::Base) do
+      include UploaderHelper
+
+      storage :file
+    end
+
+    example_uploader.new
+  end
+
+  def upload_fixture(filename)
+    fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+  end
+
+  describe '#image_or_video?' do
+    it 'returns true for an image file' do
+      uploader.store!(upload_fixture('dk.png'))
+
+      expect(uploader).to be_image_or_video
+    end
+
+    it 'it returns true for a video file' do
+      uploader.store!(upload_fixture('video_sample.mp4'))
+
+      expect(uploader).to be_image_or_video
+    end
+
+    it 'returns false for other extensions' do
+      uploader.store!(upload_fixture('doc_sample.txt'))
+
+      expect(uploader).not_to be_image_or_video
+    end
+  end
+end
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c62450fb8e2e651603c0553917df239dc3d5175d
--- /dev/null
+++ b/spec/views/ci/status/_badge.html.haml_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'ci/status/_badge', :view do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, :private) }
+  let(:pipeline) { create(:ci_pipeline, project: project) }
+
+  context 'when rendering status for build' do
+    let(:build) do
+      create(:ci_build, :success, pipeline: pipeline)
+    end
+
+    context 'when user has ability to see details' do
+      before do
+        project.add_developer(user)
+      end
+
+      it 'has link to build details page' do
+        details_path = namespace_project_build_path(
+          project.namespace, project, build)
+
+        render_status(build)
+
+        expect(rendered).to have_link 'passed', href: details_path
+      end
+    end
+
+    context 'when user do not have ability to see build details' do
+      before do
+        render_status(build)
+      end
+
+      it 'contains build status text' do
+        expect(rendered).to have_content 'passed'
+      end
+
+      it 'does not contain links' do
+        expect(rendered).not_to have_link 'passed'
+      end
+    end
+  end
+
+  context 'when rendering status for external job' do
+    context 'when user has ability to see commit status details' do
+      before do
+        project.add_developer(user)
+      end
+
+      context 'status has external target url' do
+        before do
+          external_job = create(:generic_commit_status,
+                                status: :running,
+                                pipeline: pipeline,
+                                target_url: 'http://gitlab.com')
+
+          render_status(external_job)
+        end
+
+        it 'contains valid commit status text' do
+          expect(rendered).to have_content 'running'
+        end
+
+        it 'has link to external status page' do
+          expect(rendered).to have_link 'running', href: 'http://gitlab.com'
+        end
+      end
+
+      context 'status do not have external target url' do
+        before do
+          external_job = create(:generic_commit_status, status: :canceled)
+
+          render_status(external_job)
+        end
+
+        it 'contains valid commit status text' do
+          expect(rendered).to have_content 'canceled'
+        end
+
+        it 'has link to external status page' do
+          expect(rendered).not_to have_link 'canceled'
+        end
+      end
+    end
+  end
+
+  def render_status(resource)
+    render 'ci/status/badge', status: resource.detailed_status(user)
+  end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index b6f6e7b7a2b00f33863499949ba7d4d9854187a7..ec78ac3059329e4f9f9a1719bfff55bc3bcbd9c0 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do
     it 'does not show retry button' do
       expect(rendered).not_to have_link('Retry')
     end
+
+    it 'does not show New issue button' do
+      expect(rendered).not_to have_link('New issue')
+    end
   end
 
   context 'when job is not running' do
@@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do
     it 'shows retry button' do
       expect(rendered).to have_link('Retry')
     end
+
+    context 'if build passed' do
+      it 'does not show New issue button' do
+        expect(rendered).not_to have_link('New issue')
+      end
+    end
+
+    context 'if build failed' do
+      before do
+        build.status = 'failed'
+        render
+      end
+
+      it 'shows New issue button' do
+        expect(rendered).to have_link('New issue')
+      end
+    end
   end
 
   describe 'commit title in sidebar' do
@@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do
       expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
     end
   end
+
+  describe 'New issue button' do
+    before do
+      build.status = 'failed'
+      render
+    end
+
+    it 'links to issues/new with the title and description filled in' do
+      title = "Build Failed ##{build.id}"
+      build_url = namespace_project_build_url(project.namespace, project, build)
+      href = new_namespace_project_issue_path(
+        project.namespace,
+        project,
+        issue: {
+          title: title,
+          description: build_url
+        }
+      )
+      expect(rendered).to have_link('New issue', href: href)
+    end
+  end
 end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index e741e3cf9b6c0a34d8a437fe80cde9ea97d8f188..8bc344bfbf694c1e7df2285fea31f25efaf3e8eb 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -3,11 +3,13 @@ require 'spec_helper'
 describe 'projects/commit/_commit_box.html.haml' do
   include Devise::Test::ControllerHelpers
 
+  let(:user) { create(:user) }
   let(:project) { create(:project) }
 
   before do
     assign(:project, project)
     assign(:commit, project.commit)
+    allow(view).to receive(:can_collaborate_with_project?).and_return(false)
   end
 
   it 'shows the commit SHA' do
@@ -23,6 +25,32 @@ describe 'projects/commit/_commit_box.html.haml' do
 
     render
 
-    expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
+    expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+  end
+
+  context 'viewing a commit' do
+    context 'as a developer' do
+      before do
+        expect(view).to receive(:can_collaborate_with_project?).and_return(true)
+      end
+
+      it 'has a link to create a new tag' do
+        render
+
+        expect(rendered).to have_link('Tag')
+      end
+    end
+
+    context 'as a non-developer' do
+      before do
+        expect(view).to receive(:can_collaborate_with_project?).and_return(false)
+      end
+
+      it 'does not have a link to create a new tag' do
+        render
+
+        expect(rendered).not_to have_link('Tag')
+      end
+    end
   end
 end
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index d25de8af5d2f9087d281f270c8940174a8224de2..65f9d0125e66e29b758f6ab43aa06ee08c3b02aa 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -50,4 +50,23 @@ describe 'projects/pipelines/_stage', :view do
       expect(rendered).to have_text 'test:build', count: 1
     end
   end
+
+  context 'when there are multiple builds' do
+    before do
+      HasStatus::AVAILABLE_STATUSES.each do |status|
+        create_build(status)
+      end
+    end
+
+    it 'shows them in order' do
+      render
+
+      expect(rendered).to have_text(HasStatus::ORDERED_STATUSES.join(" "))
+    end
+
+    def create_build(status)
+      create(:ci_build, name: status, status: status,
+                        pipeline: pipeline, stage: stage.name)
+    end
+  end
 end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 97c4bfcd248729b8dc18213c1e36f735642dc638..bd5cc651c2b6a2acf2acf378272573d7ae57514a 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -1,12 +1,10 @@
 require 'spec_helper'
 
 describe AuthorizedProjectsWorker do
-  let(:worker) { described_class.new }
+  let(:project) { create(:empty_project) }
 
   describe '.bulk_perform_and_wait' do
     it 'schedules the ids and waits for the jobs to complete' do
-      project = create(:project)
-
       project.owner.project_authorizations.delete_all
 
       described_class.bulk_perform_and_wait([[project.owner.id]])
@@ -15,20 +13,37 @@ describe AuthorizedProjectsWorker do
     end
   end
 
+  describe '.bulk_perform_async' do
+    it "uses it's respective sidekiq queue" do
+      args = [[project.owner.id]]
+      push_bulk_args = {
+        'class' => described_class,
+        'queue' => described_class.sidekiq_options['queue'],
+        'args' => args
+      }
+
+      expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once
+
+      described_class.bulk_perform_async(args)
+    end
+  end
+
   describe '#perform' do
+    subject { described_class.new }
+
     it "refreshes user's authorized projects" do
       user = create(:user)
 
       expect_any_instance_of(User).to receive(:refresh_authorized_projects)
 
-      worker.perform(user.id)
+      subject.perform(user.id)
     end
 
     context "when the user is not found" do
       it "does nothing" do
         expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
 
-        described_class.new.perform(-1)
+        subject.perform(-1)
       end
     end
   end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
deleted file mode 100644
index 542e674c15015e9b9c6c51863022a6aac5955544..0000000000000000000000000000000000000000
--- a/spec/workers/build_email_worker_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe BuildEmailWorker do
-  include EmailHelpers
-  include RepoHelpers
-
-  let(:build) { create(:ci_build) }
-  let(:user) { create(:user) }
-  let(:data) { Gitlab::DataBuilder::Build.build(build) }
-
-  subject { BuildEmailWorker.new }
-
-  before do
-    allow(build).to receive(:execute_hooks).and_return(false)
-    build.success
-  end
-
-  describe "#perform" do
-    it "sends mail" do
-      subject.perform(build.id, [user.email], data.stringify_keys)
-
-      email = ActionMailer::Base.deliveries.last
-      expect(email.subject).to include('Build success for')
-      expect(email.to).to eq([user.email])
-    end
-
-    it "gracefully handles an input SMTP error" do
-      reset_delivered_emails!
-      allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
-
-      subject.perform(build.id, [user.email], data.stringify_keys)
-
-      expect(ActionMailer::Base.deliveries.count).to eq(0)
-    end
-  end
-end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 5ef8cf1105b16101f9cb9fd9e7d862ea6d4da0cf..a60af574a08e36e5eba4cb4df848eee9ff09b780 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -102,8 +102,8 @@ describe GitGarbageCollectWorker do
     new_commit_sha = Rugged::Commit.create(
       rugged,
       message: "hello world #{SecureRandom.hex(6)}",
-      author: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
-      committer: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
+      author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
+      committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
       tree: old_commit.tree,
       parents: [old_commit],
     )
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 5919b99a6ed5652f67e37026ddcefbd7b10f1020..7bcb552120210a04e38842601abdd6c368645279 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -105,6 +105,6 @@ describe PostReceive do
   end
 
   def pwd(project)
-    File.join(Gitlab.config.repositories.storages.default, project.path_with_namespace)
+    File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace)
   end
 end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 60605460adbebde182e448888bf0a1ee41f09651..87521ae408e90b585002c0429b57a0d23ac25088 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -15,24 +15,24 @@ describe RepositoryForkWorker do
     it "creates a new repository from a fork" do
       expect(shell).to receive(:fork_repository).with(
         '/test/path',
-        project.path_with_namespace,
+        project.full_path,
         project.repository_storage_path,
-        fork_project.namespace.path
+        fork_project.namespace.full_path
       ).and_return(true)
 
       subject.perform(
         project.id,
         '/test/path',
-        project.path_with_namespace,
-        fork_project.namespace.path)
+        project.full_path,
+        fork_project.namespace.full_path)
     end
 
     it 'flushes various caches' do
       expect(shell).to receive(:fork_repository).with(
         '/test/path',
-        project.path_with_namespace,
+        project.full_path,
         project.repository_storage_path,
-        fork_project.namespace.path
+        fork_project.namespace.full_path
       ).and_return(true)
 
       expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
@@ -41,8 +41,8 @@ describe RepositoryForkWorker do
       expect_any_instance_of(Repository).to receive(:expire_exists_cache).
         and_call_original
 
-      subject.perform(project.id, '/test/path', project.path_with_namespace,
-                      fork_project.namespace.path)
+      subject.perform(project.id, '/test/path', project.full_path,
+                      fork_project.namespace.full_path)
     end
 
     it "handles bad fork" do
@@ -53,8 +53,8 @@ describe RepositoryForkWorker do
       subject.perform(
         project.id,
         '/test/path',
-        project.path_with_namespace,
-        fork_project.namespace.path)
+        project.full_path,
+        fork_project.namespace.full_path)
     end
   end
 end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index f1b1574abf487fa21cdadf19dc04553e332f8c58..c42f3147b7ae6042c0c57f956d97d86458b73e67 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -20,7 +20,7 @@ describe RepositoryImportWorker do
 
     context 'when the import has failed' do
       it 'hide the credentials that were used in the import URL' do
-        error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+        error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
         expect_any_instance_of(Projects::ImportService).to receive(:execute).
           and_return({ status: :error, message: error })
 
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
deleted file mode 100644
index 801fa31b45d8db39becde154f01cf1d2b86fd4e9..0000000000000000000000000000000000000000
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require "spec_helper"
-
-describe StuckCiBuildsWorker do
-  let!(:build) { create :ci_build }
-  let(:worker) { described_class.new }
-
-  subject do
-    build.reload
-    build.status
-  end
-
-  %w(pending running).each do |status|
-    context "#{status} build" do
-      before do
-        build.update!(status: status)
-      end
-
-      it 'gets dropped if it was updated over 2 days ago' do
-        build.update!(updated_at: 2.days.ago)
-        worker.perform
-        is_expected.to eq('failed')
-      end
-
-      it "is still #{status}" do
-        build.update!(updated_at: 1.minute.ago)
-        worker.perform
-        is_expected.to eq(status)
-      end
-    end
-  end
-
-  %w(success failed canceled).each do |status|
-    context "#{status} build" do
-      before do
-        build.update!(status: status)
-      end
-
-      it "is still #{status}" do
-        build.update!(updated_at: 2.days.ago)
-        worker.perform
-        is_expected.to eq(status)
-      end
-    end
-  end
-
-  context "for deleted project" do
-    before do
-      build.update!(status: :running, updated_at: 2.days.ago)
-      build.project.update(pending_delete: true)
-    end
-
-    it "does not drop build" do
-      expect_any_instance_of(Ci::Build).not_to receive(:drop)
-      worker.perform
-    end
-  end
-end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8434b0c8e5b15fb6cc295010a4b303e73cd3256f
--- /dev/null
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -0,0 +1,129 @@
+require 'spec_helper'
+
+describe StuckCiJobsWorker do
+  let!(:runner) { create :ci_runner }
+  let!(:job) { create :ci_build, runner: runner }
+  let(:worker) { described_class.new }
+  let(:exclusive_lease_uuid) { SecureRandom.uuid }
+
+  subject do
+    job.reload
+    job.status
+  end
+
+  before do
+    job.update!(status: status, updated_at: updated_at)
+    allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
+  end
+
+  shared_examples 'job is dropped' do
+    it 'changes status' do
+      worker.perform
+      is_expected.to eq('failed')
+    end
+  end
+
+  shared_examples 'job is unchanged' do
+    it "doesn't change status" do
+      worker.perform
+      is_expected.to eq(status)
+    end
+  end
+
+  context 'when job is pending' do
+    let(:status) { 'pending' }
+
+    context 'when job is not stuck' do
+      before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) }
+
+      context 'when job was not updated for more than 1 day ago' do
+        let(:updated_at) { 2.days.ago }
+        it_behaves_like 'job is dropped'
+      end
+
+      context 'when job was updated in less than 1 day ago' do
+        let(:updated_at) { 6.hours.ago }
+        it_behaves_like 'job is unchanged'
+      end
+
+      context 'when job was not updated for more than 1 hour ago' do
+        let(:updated_at) { 2.hours.ago }
+        it_behaves_like 'job is unchanged'
+      end
+    end
+
+    context 'when job is stuck' do
+      before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) }
+
+      context 'when job was not updated for more than 1 hour ago' do
+        let(:updated_at) { 2.hours.ago }
+        it_behaves_like 'job is dropped'
+      end
+
+      context 'when job was updated in less than 1 hour ago' do
+        let(:updated_at) { 30.minutes.ago }
+        it_behaves_like 'job is unchanged'
+      end
+    end
+  end
+
+  context 'when job is running' do
+    let(:status) { 'running' }
+
+    context 'when job was not updated for more than 1 hour ago' do
+      let(:updated_at) { 2.hours.ago }
+      it_behaves_like 'job is dropped'
+    end
+
+    context 'when job was updated in less than 1 hour ago' do
+      let(:updated_at) { 30.minutes.ago }
+      it_behaves_like 'job is unchanged'
+    end
+  end
+
+  %w(success skipped failed canceled).each do |status|
+    context "when job is #{status}" do
+      let(:status) { status }
+      let(:updated_at) { 2.days.ago }
+      it_behaves_like 'job is unchanged'
+    end
+  end
+
+  context 'for deleted project' do
+    let(:status) { 'running' }
+    let(:updated_at) { 2.days.ago }
+
+    before { job.project.update(pending_delete: true) }
+
+    it 'does not drop job' do
+      expect_any_instance_of(Ci::Build).not_to receive(:drop)
+      worker.perform
+    end
+  end
+
+  describe 'exclusive lease' do
+    let(:status) { 'running' }
+    let(:updated_at) { 2.days.ago }
+    let(:worker2) { described_class.new }
+
+    it 'is guard by exclusive lease when executed concurrently' do
+      expect(worker).to receive(:drop).at_least(:once)
+      expect(worker2).not_to receive(:drop)
+      worker.perform
+      allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
+      worker2.perform
+    end
+
+    it 'can be executed in sequence' do
+      expect(worker).to receive(:drop).at_least(:once)
+      expect(worker2).to receive(:drop).at_least(:once)
+      worker.perform
+      worker2.perform
+    end
+
+    it 'cancels exclusive lease after worker perform' do
+      expect(Gitlab::ExclusiveLease).to receive(:cancel).with(described_class::EXCLUSIVE_LEASE_KEY, exclusive_lease_uuid)
+      worker.perform
+    end
+  end
+end
diff --git a/spec/workers/system_hook_push_worker_spec.rb b/spec/workers/system_hook_push_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b1d446ed25fbe9da27fb1411ffdcbcd9754cbba4
--- /dev/null
+++ b/spec/workers/system_hook_push_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe SystemHookPushWorker do
+  include RepoHelpers
+
+  subject { described_class.new }
+
+  describe '#perform' do
+    it 'executes SystemHooksService with expected values' do
+      push_data = double('push_data')
+      system_hook_service = double('system_hook_service')
+
+      expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
+      expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
+
+      subject.perform(push_data, :push_hooks)
+    end
+  end
+end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index c78a69eda675a464dc71f9157da7855c54a19e47..262d6e5a9abd332cb23625d133f7c3e3bb355d12 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -23,16 +23,5 @@ describe UpdateMergeRequestsWorker do
 
       perform
     end
-
-    it 'executes SystemHooksService with expected values' do
-      push_data = double('push_data')
-      expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data)
-
-      system_hook_service = double('system_hook_service')
-      expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
-      expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
-
-      perform
-    end
   end
 end
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..911360da66ca32147f6140244ed5287b6b80362b
--- /dev/null
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe UploadChecksumWorker do
+  describe '#perform' do
+    it 'rescues ActiveRecord::RecordNotFound' do
+      expect { described_class.new.perform(999_999) }.not_to raise_error
+    end
+
+    it 'calls calculate_checksum_without_delay and save!' do
+      upload = spy
+      expect(Upload).to receive(:find).with(999_999).and_return(upload)
+
+      described_class.new.perform(999_999)
+
+      expect(upload).to have_received(:calculate_checksum)
+      expect(upload).to have_received(:save!)
+    end
+  end
+end
diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js
deleted file mode 100644
index 166bd654d6eec435e5990baaf70e6f477e16e5e5..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/g.bar.js
+++ /dev/null
@@ -1,674 +0,0 @@
-/*!
- * g.Raphael 0.51 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- */
-(function () {
-    var mmin = Math.min,
-        mmax = Math.max;
-
-    function finger(x, y, width, height, dir, ending, isPath, paper) {
-        var path,
-            ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' };
-
-        // dir 0 for horizontal and 1 for vertical
-        if ((dir && !height) || (!dir && !width)) {
-            return isPath ? "" : paper.path();
-        }
-
-        ending = ends[ending] || "square";
-        height = Math.round(height);
-        width = Math.round(width);
-        x = Math.round(x);
-        y = Math.round(y);
-
-        switch (ending) {
-            case "round":
-                if (!dir) {
-                    var r = ~~(height / 2);
-
-                    if (width < r) {
-                        r = width;
-                        path = [
-                            "M", x + .5, y + .5 - ~~(height / 2),
-                            "l", 0, 0,
-                            "a", r, ~~(height / 2), 0, 0, 1, 0, height,
-                            "l", 0, 0,
-                            "z"
-                        ];
-                    } else {
-                        path = [
-                            "M", x + .5, y + .5 - r,
-                            "l", width - r, 0,
-                            "a", r, r, 0, 1, 1, 0, height,
-                            "l", r - width, 0,
-                            "z"
-                        ];
-                    }
-                } else {
-                    r = ~~(width / 2);
-
-                    if (height < r) {
-                        r = height;
-                        path = [
-                            "M", x - ~~(width / 2), y,
-                            "l", 0, 0,
-                            "a", ~~(width / 2), r, 0, 0, 1, width, 0,
-                            "l", 0, 0,
-                            "z"
-                        ];
-                    } else {
-                        path = [
-                            "M", x - r, y,
-                            "l", 0, r - height,
-                            "a", r, r, 0, 1, 1, width, 0,
-                            "l", 0, height - r,
-                            "z"
-                        ];
-                    }
-                }
-                break;
-            case "sharp":
-                if (!dir) {
-                    var half = ~~(height / 2);
-
-                    path = [
-                        "M", x, y + half,
-                        "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height),
-                        "z"
-                    ];
-                } else {
-                    half = ~~(width / 2);
-                    path = [
-                        "M", x + half, y,
-                        "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half,
-                        "z"
-                    ];
-                }
-                break;
-            case "square":
-                if (!dir) {
-                    path = [
-                        "M", x, y + ~~(height / 2),
-                        "l", 0, -height, width, 0, 0, height,
-                        "z"
-                    ];
-                } else {
-                    path = [
-                        "M", x + ~~(width / 2), y,
-                        "l", 1 - width, 0, 0, -height, width - 1, 0,
-                        "z"
-                    ];
-                }
-                break;
-            case "soft":
-                if (!dir) {
-                    r = mmin(width, Math.round(height / 5));
-                    path = [
-                        "M", x + .5, y + .5 - ~~(height / 2),
-                        "l", width - r, 0,
-                        "a", r, r, 0, 0, 1, r, r,
-                        "l", 0, height - r * 2,
-                        "a", r, r, 0, 0, 1, -r, r,
-                        "l", r - width, 0,
-                        "z"
-                    ];
-                } else {
-                    r = mmin(Math.round(width / 5), height);
-                    path = [
-                        "M", x - ~~(width / 2), y,
-                        "l", 0, r - height,
-                        "a", r, r, 0, 0, 1, r, -r,
-                        "l", width - 2 * r, 0,
-                        "a", r, r, 0, 0, 1, r, r,
-                        "l", 0, height - r,
-                        "z"
-                    ];
-                }
-        }
-
-        if (isPath) {
-            return path.join(",");
-        } else {
-            return paper.path(path);
-        }
-    }
-
-/*\
- * Paper.vbarchart
- [ method ]
- **
- * Creates a vertical bar chart
- **
- > Parameters
- **
- - x (number) x coordinate of the chart
- - y (number) y coordinate of the chart
- - width (number) width of the chart (respected by all elements in the set)
- - height (number) height of the chart (respected by all elements in the set)
- - values (array) values
- - opts (object) options for the chart
- o {
- o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
- o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
- o vgutter (number)
- o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
- o stacked (boolean) whether or not to tread values as in a stacked bar chart
- o to
- o stretch (boolean)
- o }
- **
- = (object) path element of the popup
- > Usage
- | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
- \*/
- 
-    function VBarchart(paper, x, y, width, height, values, opts) {
-        opts = opts || {};
-
-        var chartinst = this,
-            type = opts.type || "square",
-            gutter = parseFloat(opts.gutter || "20%"),
-            chart = paper.set(),
-            bars = paper.set(),
-            covers = paper.set(),
-            covers2 = paper.set(),
-            total = Math.max.apply(Math, values),
-            stacktotal = [],
-            multi = 0,
-            colors = opts.colors || chartinst.colors,
-            len = values.length;
-
-        if (Raphael.is(values[0], "array")) {
-            total = [];
-            multi = len;
-            len = 0;
-
-            for (var i = values.length; i--;) {
-                bars.push(paper.set());
-                total.push(Math.max.apply(Math, values[i]));
-                len = Math.max(len, values[i].length);
-            }
-
-            if (opts.stacked) {
-                for (var i = len; i--;) {
-                    var tot = 0;
-
-                    for (var j = values.length; j--;) {
-                        tot +=+ values[j][i] || 0;
-                    }
-
-                    stacktotal.push(tot);
-                }
-            }
-
-            for (var i = values.length; i--;) {
-                if (values[i].length < len) {
-                    for (var j = len; j--;) {
-                        values[i].push(0);
-                    }
-                }
-            }
-
-            total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
-        }
-        
-        total = (opts.to) || total;
-
-        var barwidth = width / (len * (100 + gutter) + gutter) * 100,
-            barhgutter = barwidth * gutter / 100,
-            barvgutter = opts.vgutter == null ? 20 : opts.vgutter,
-            stack = [],
-            X = x + barhgutter,
-            Y = (height - 2 * barvgutter) / total;
-
-        if (!opts.stretch) {
-            barhgutter = Math.round(barhgutter);
-            barwidth = Math.floor(barwidth);
-        }
-
-        !opts.stacked && (barwidth /= multi || 1);
-
-        for (var i = 0; i < len; i++) {
-            stack = [];
-
-            for (var j = 0; j < (multi || 1); j++) {
-                var h = Math.round((multi ? values[j][i] : values[i]) * Y),
-                    top = y + height - barvgutter - h,
-                    bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] });
-
-                if (multi) {
-                    bars[j].push(bar);
-                } else {
-                    bars.push(bar);
-                }
-
-                bar.y = top;
-                bar.x = Math.round(X + barwidth / 2);
-                bar.w = barwidth;
-                bar.h = h;
-                bar.value = multi ? values[j][i] : values[i];
-
-                if (!opts.stacked) {
-                    X += barwidth;
-                } else {
-                    stack.push(bar);
-                }
-            }
-
-            if (opts.stacked) {
-                var cvr;
-
-                covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim));
-                cvr.bars = paper.set();
-
-                var size = 0;
-
-                for (var s = stack.length; s--;) {
-                    stack[s].toFront();
-                }
-
-                for (var s = 0, ss = stack.length; s < ss; s++) {
-                    var bar = stack[s],
-                        cover,
-                        h = (size + bar.value) * Y,
-                        path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper);
-
-                    cvr.bars.push(bar);
-                    size && bar.attr({path: path});
-                    bar.h = h;
-                    bar.y = y + height - barvgutter - !!size * .5 - h;
-                    covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim));
-                    cover.bar = bar;
-                    cover.value = bar.value;
-                    size += bar.value;
-                }
-
-                X += barwidth;
-            }
-
-            X += barhgutter;
-        }
-
-        covers2.toFront();
-        X = x + barhgutter;
-
-        if (!opts.stacked) {
-            for (var i = 0; i < len; i++) {
-                for (var j = 0; j < (multi || 1); j++) {
-                    var cover;
-
-                    covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim));
-                    cover.bar = multi ? bars[j][i] : bars[i];
-                    cover.value = cover.bar.value;
-                    X += barwidth;
-                }
-
-                X += barhgutter;
-            }
-        }
-
-        chart.label = function (labels, isBottom) {
-            labels = labels || [];
-            this.labels = paper.set();
-
-            var L, l = -Infinity;
-
-            if (opts.stacked) {
-                for (var i = 0; i < len; i++) {
-                    var tot = 0;
-
-                    for (var j = 0; j < (multi || 1); j++) {
-                        tot += multi ? values[j][i] : values[i];
-
-                        if (j == multi - 1) {
-                            var label = paper.labelise(labels[i], tot, total);
-
-                            L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
-
-                            var bb = L.getBBox();
-
-                            if (bb.x - 7 < l) {
-                                L.remove();
-                            } else {
-                                this.labels.push(L);
-                                l = bb.x + bb.width;
-                            }
-                        }
-                    }
-                }
-            } else {
-                for (var i = 0; i < len; i++) {
-                    for (var j = 0; j < (multi || 1); j++) {
-                        var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
-
-                        L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
-
-                        var bb = L.getBBox();
-
-                        if (bb.x - 7 < l) {
-                            L.remove();
-                        } else {
-                            this.labels.push(L);
-                            l = bb.x + bb.width;
-                        }
-                    }
-                }
-            }
-            return this;
-        };
-
-        chart.hover = function (fin, fout) {
-            covers2.hide();
-            covers.show();
-            covers.mouseover(fin).mouseout(fout);
-            return this;
-        };
-
-        chart.hoverColumn = function (fin, fout) {
-            covers.hide();
-            covers2.show();
-            fout = fout || function () {};
-            covers2.mouseover(fin).mouseout(fout);
-            return this;
-        };
-
-        chart.click = function (f) {
-            covers2.hide();
-            covers.show();
-            covers.click(f);
-            return this;
-        };
-
-        chart.each = function (f) {
-            if (!Raphael.is(f, "function")) {
-                return this;
-            }
-            for (var i = covers.length; i--;) {
-                f.call(covers[i]);
-            }
-            return this;
-        };
-
-        chart.eachColumn = function (f) {
-            if (!Raphael.is(f, "function")) {
-                return this;
-            }
-            for (var i = covers2.length; i--;) {
-                f.call(covers2[i]);
-            }
-            return this;
-        };
-
-        chart.clickColumn = function (f) {
-            covers.hide();
-            covers2.show();
-            covers2.click(f);
-            return this;
-        };
-
-        chart.push(bars, covers, covers2);
-        chart.bars = bars;
-        chart.covers = covers;
-        return chart;
-    };
-    
-    //inheritance
-    var F = function() {};
-    F.prototype = Raphael.g;
-    HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart
-    
-    Raphael.fn.barchart = function(x, y, width, height, values, opts) {
-        return new VBarchart(this, x, y, width, height, values, opts);
-    };
-    
-/*\
- * Paper.barchart
- [ method ]
- **
- * Creates a horizontal bar chart
- **
- > Parameters
- **
- - x (number) x coordinate of the chart
- - y (number) y coordinate of the chart
- - width (number) width of the chart (respected by all elements in the set)
- - height (number) height of the chart (respected by all elements in the set)
- - values (array) values
- - opts (object) options for the chart
- o {
- o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
- o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
- o vgutter (number)
- o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
- o stacked (boolean) whether or not to tread values as in a stacked bar chart
- o to
- o stretch (boolean)
- o }
- **
- = (object) path element of the popup
- > Usage
- | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
- \*/
- 
-    function HBarchart(paper, x, y, width, height, values, opts) {
-        opts = opts || {};
-
-        var chartinst = this,
-            type = opts.type || "square",
-            gutter = parseFloat(opts.gutter || "20%"),
-            chart = paper.set(),
-            bars = paper.set(),
-            covers = paper.set(),
-            covers2 = paper.set(),
-            total = Math.max.apply(Math, values),
-            stacktotal = [],
-            multi = 0,
-            colors = opts.colors || chartinst.colors,
-            len = values.length;
-
-        if (Raphael.is(values[0], "array")) {
-            total = [];
-            multi = len;
-            len = 0;
-
-            for (var i = values.length; i--;) {
-                bars.push(paper.set());
-                total.push(Math.max.apply(Math, values[i]));
-                len = Math.max(len, values[i].length);
-            }
-
-            if (opts.stacked) {
-                for (var i = len; i--;) {
-                    var tot = 0;
-                    for (var j = values.length; j--;) {
-                        tot +=+ values[j][i] || 0;
-                    }
-                    stacktotal.push(tot);
-                }
-            }
-
-            for (var i = values.length; i--;) {
-                if (values[i].length < len) {
-                    for (var j = len; j--;) {
-                        values[i].push(0);
-                    }
-                }
-            }
-
-            total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
-        }
-        
-        total = (opts.to) || total;
-
-        var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
-            bargutter = Math.floor(barheight * gutter / 100),
-            stack = [],
-            Y = y + bargutter,
-            X = (width - 1) / total;
-
-        !opts.stacked && (barheight /= multi || 1);
-
-        for (var i = 0; i < len; i++) {
-            stack = [];
-
-            for (var j = 0; j < (multi || 1); j++) {
-                var val = multi ? values[j][i] : values[i],
-                    bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]});
-
-                if (multi) {
-                    bars[j].push(bar);
-                } else {
-                    bars.push(bar);
-                }
-
-                bar.x = x + Math.round(val * X);
-                bar.y = Y + barheight / 2;
-                bar.w = Math.round(val * X);
-                bar.h = barheight;
-                bar.value = +val;
-
-                if (!opts.stacked) {
-                    Y += barheight;
-                } else {
-                    stack.push(bar);
-                }
-            }
-
-            if (opts.stacked) {
-                var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim);
-
-                covers2.push(cvr);
-                cvr.bars = paper.set();
-
-                var size = 0;
-
-                for (var s = stack.length; s--;) {
-                    stack[s].toFront();
-                }
-
-                for (var s = 0, ss = stack.length; s < ss; s++) {
-                    var bar = stack[s],
-                        cover,
-                        val = Math.round((size + bar.value) * X),
-                        path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper);
-
-                    cvr.bars.push(bar);
-                    size && bar.attr({ path: path });
-                    bar.w = val;
-                    bar.x = x + val;
-                    covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim));
-                    cover.bar = bar;
-                    size += bar.value;
-                }
-
-                Y += barheight;
-            }
-
-            Y += bargutter;
-        }
-
-        covers2.toFront();
-        Y = y + bargutter;
-
-        if (!opts.stacked) {
-            for (var i = 0; i < len; i++) {
-                for (var j = 0; j < (multi || 1); j++) {
-                    var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim);
-
-                    covers.push(cover);
-                    cover.bar = multi ? bars[j][i] : bars[i];
-                    cover.value = cover.bar.value;
-                    Y += barheight;
-                }
-
-                Y += bargutter;
-            }
-        }
-
-        chart.label = function (labels, isRight) {
-            labels = labels || [];
-            this.labels = paper.set();
-
-            for (var i = 0; i < len; i++) {
-                for (var j = 0; j < multi; j++) {
-                    var  label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total),
-                        X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
-                        A = isRight ? "end" : "start",
-                        L;
-
-                    this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0]));
-
-                    if (L.getBBox().x < x + 5) {
-                        L.attr({x: x + 5, "text-anchor": "start"});
-                    } else {
-                        bars[i * (multi || 1) + j].label = L;
-                    }
-                }
-            }
-
-            return this;
-        };
-
-        chart.hover = function (fin, fout) {
-            covers2.hide();
-            covers.show();
-            fout = fout || function () {};
-            covers.mouseover(fin).mouseout(fout);
-            return this;
-        };
-
-        chart.hoverColumn = function (fin, fout) {
-            covers.hide();
-            covers2.show();
-            fout = fout || function () {};
-            covers2.mouseover(fin).mouseout(fout);
-            return this;
-        };
-
-        chart.each = function (f) {
-            if (!Raphael.is(f, "function")) {
-                return this;
-            }
-            for (var i = covers.length; i--;) {
-                f.call(covers[i]);
-            }
-            return this;
-        };
-
-        chart.eachColumn = function (f) {
-            if (!Raphael.is(f, "function")) {
-                return this;
-            }
-            for (var i = covers2.length; i--;) {
-                f.call(covers2[i]);
-            }
-            return this;
-        };
-
-        chart.click = function (f) {
-            covers2.hide();
-            covers.show();
-            covers.click(f);
-            return this;
-        };
-
-        chart.clickColumn = function (f) {
-            covers.hide();
-            covers2.show();
-            covers2.click(f);
-            return this;
-        };
-
-        chart.push(bars, covers, covers2);
-        chart.bars = bars;
-        chart.covers = covers;
-        return chart;
-    };
-    
-    Raphael.fn.hbarchart = function(x, y, width, height, values, opts) {
-        return new HBarchart(this, x, y, width, height, values, opts);
-    };
-    
-})();
diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js
deleted file mode 100644
index 27f27caf9f2c8321120f9ce263deb847f8f46971..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/g.raphael.js
+++ /dev/null
@@ -1,861 +0,0 @@
-/*!
- * g.Raphael 0.51 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- */
-
-/*
- * Tooltips on Element prototype
- */
-/*\
- * Element.popup
- [ method ]
- **
- * Puts the context Element in a 'popup' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
- - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`]
- - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`]
- - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`]
- **
- = (object) path element of the popup
- \*/
-Raphael.el.popup = function (dir, size, x, y) {
-    var paper = this.paper || this[0].paper,
-        bb, xy, center, cw, ch;
-
-    if (!paper) return;
-
-    switch (this.type) {
-        case 'text':
-        case 'circle':
-        case 'ellipse': center = true; break;
-        default: center = false;
-    }
-
-    dir = dir == null ? 'up' : dir;
-    size = size || 5;
-    bb = this.getBBox();
-
-    x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
-    y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
-    cw = Math.max(bb.width / 2 - size, 0);
-    ch = Math.max(bb.height / 2 - size, 0);
-
-    this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0));
-    bb = this.getBBox();
-
-    var paths = {
-        up: [
-            'M', x, y,
-            'l', -size, -size, -cw, 0,
-            'a', size, size, 0, 0, 1, -size, -size,
-            'l', 0, -bb.height,
-            'a', size, size, 0, 0, 1, size, -size,
-            'l', size * 2 + cw * 2, 0,
-            'a', size, size, 0, 0, 1, size, size,
-            'l', 0, bb.height,
-            'a', size, size, 0, 0, 1, -size, size,
-            'l', -cw, 0,
-            'z'
-        ].join(','),
-        down: [
-            'M', x, y,
-            'l', size, size, cw, 0,
-            'a', size, size, 0, 0, 1, size, size,
-            'l', 0, bb.height,
-            'a', size, size, 0, 0, 1, -size, size,
-            'l', -(size * 2 + cw * 2), 0,
-            'a', size, size, 0, 0, 1, -size, -size,
-            'l', 0, -bb.height,
-            'a', size, size, 0, 0, 1, size, -size,
-            'l', cw, 0,
-            'z'
-        ].join(','),
-        left: [
-            'M', x, y,
-            'l', -size, size, 0, ch,
-            'a', size, size, 0, 0, 1, -size, size,
-            'l', -bb.width, 0,
-            'a', size, size, 0, 0, 1, -size, -size,
-            'l', 0, -(size * 2 + ch * 2),
-            'a', size, size, 0, 0, 1, size, -size,
-            'l', bb.width, 0,
-            'a', size, size, 0, 0, 1, size, size,
-            'l', 0, ch,
-            'z'
-        ].join(','),
-        right: [
-            'M', x, y,
-            'l', size, -size, 0, -ch,
-            'a', size, size, 0, 0, 1, size, -size,
-            'l', bb.width, 0,
-            'a', size, size, 0, 0, 1, size, size,
-            'l', 0, size * 2 + ch * 2,
-            'a', size, size, 0, 0, 1, -size, size,
-            'l', -bb.width, 0,
-            'a', size, size, 0, 0, 1, -size, -size,
-            'l', 0, -ch,
-            'z'
-        ].join(',')
-    };
-
-    xy = {
-        up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) },
-        down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) },
-        left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) },
-        right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }
-    }[dir];
-
-    this.translate(xy.x, xy.y);
-    return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.tag
- [ method ]
- **
- * Puts the context Element in a 'tag' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - r (number) radius of the loop [default: `5`]
- - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`]
- - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`]
- **
- = (object) path element of the tag
- \*/
-Raphael.el.tag = function (angle, r, x, y) {
-    var d = 3,
-        paper = this.paper || this[0].paper;
-
-    if (!paper) return;
-
-    var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
-        bb = this.getBBox(),
-        dx, R, center, tmp;
-
-    switch (this.type) {
-        case 'text':
-        case 'circle':
-        case 'ellipse': center = true; break;
-        default: center = false;
-    }
-
-    angle = angle || 0;
-    x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
-    y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
-    r = r == null ? 5 : r;
-    R = .5522 * r;
-
-    if (bb.height >= r * 2) {
-        p.attr({
-            path: [
-                "M", x, y + r,
-                "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
-                "m", 0, -r * 2 -d,
-                "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2,
-                "L", x + r + d, y + bb.height / 2 + d,
-                "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0,
-                "L", x, y - r - d
-            ].join(",")
-        });
-    } else {
-        dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
-        p.attr({
-            path: [
-                "M", x, y + r,
-                "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r,
-                "M", x + dx, y - bb.height / 2 - d,
-                "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d,
-                "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d,
-                "L", x + dx, y - bb.height / 2 - d
-            ].join(",")
-        });
-    }
-
-    angle = 360 - angle;
-    p.rotate(angle, x, y);
-
-    if (this.attrs) {
-        //elements
-        this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
-        this.rotate(angle, x, y);
-        angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
-    } else {
-        //sets
-        if (angle > 90 && angle < 270) {
-            this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2);
-            this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2);
-        } else {
-            this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2);
-            this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2); 
-        }
-    }
-
-    return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.drop
- [ method ]
- **
- * Puts the context Element in a 'drop' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`]
- - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`]
- **
- = (object) path element of the drop
- \*/
-Raphael.el.drop = function (angle, x, y) {
-    var bb = this.getBBox(),
-        paper = this.paper || this[0].paper,
-        center, size, p, dx, dy;
-
-    if (!paper) return;
-
-    switch (this.type) {
-        case 'text':
-        case 'circle':
-        case 'ellipse': center = true; break;
-        default: center = false;
-    }
-
-    angle = angle || 0;
-
-    x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
-    y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
-    size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height);
-    p = paper.path([
-        "M", x, y,
-        "l", size, 0,
-        "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7,
-        "z"
-    ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y);
-
-    angle = (angle + 90) * Math.PI / 180;
-    dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2);
-    dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2);
-
-    this.attrs ?
-        this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) :
-        this.translate(dx - bb.x, dy - bb.y);
-
-    return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.flag
- [ method ]
- **
- * Puts the context Element in a 'flag' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`]
- - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`]
- **
- = (object) path element of the flag
- \*/
-Raphael.el.flag = function (angle, x, y) {
-    var d = 3,
-        paper = this.paper || this[0].paper;
-
-    if (!paper) return;
-
-    var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
-        bb = this.getBBox(),
-        h = bb.height / 2,
-        center;
-
-    switch (this.type) {
-        case 'text':
-        case 'circle':
-        case 'ellipse': center = true; break;
-        default: center = false;
-    }
-
-    angle = angle || 0;
-    x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
-    y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y);
-
-    p.attr({
-        path: [
-            "M", x, y,
-            "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0,
-            "z"
-        ].join(",")
-    });
-
-    angle = 360 - angle;
-    p.rotate(angle, x, y);
-
-    if (this.attrs) {
-        //elements
-        this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
-        this.rotate(angle, x, y);
-        angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
-    } else {
-        //sets
-        if (angle > 90 && angle < 270) {
-            this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2);
-            this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2);
-        } else {
-            this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2);
-            this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2);
-        }
-    }
-
-    return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.label
- [ method ]
- **
- * Puts the context Element in a 'label' tooltip. Can also be used on sets.
- **
- = (object) path element of the label.
- \*/
-Raphael.el.label = function () {
-    var bb = this.getBBox(),
-        paper = this.paper || this[0].paper,
-        r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
-
-    if (!paper) return;
-
-    return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.blob
- [ method ]
- **
- * Puts the context Element in a 'blob' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`]
- - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`]
- **
- = (object) path element of the blob
- \*/
-Raphael.el.blob = function (angle, x, y) {
-    var bb = this.getBBox(),
-        rad = Math.PI / 180,
-        paper = this.paper || this[0].paper,
-        p, center, size;
-
-    if (!paper) return;
-
-    switch (this.type) {
-        case 'text':
-        case 'circle':
-        case 'ellipse': center = true; break;
-        default: center = false;
-    }
-
-    p = paper.path().attr({ fill: "#000", stroke: "none" });
-    angle = (+angle + 1 ? angle : 45) + 90;
-    size = Math.min(bb.height, bb.width);
-    x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
-    y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
-
-    var w = Math.max(bb.width + size, size * 25 / 12),
-        h = Math.max(bb.height + size, size * 25 / 12),
-        x2 = x + size * Math.sin((angle - 22.5) * rad),
-        y2 = y + size * Math.cos((angle - 22.5) * rad),
-        x1 = x + size * Math.sin((angle + 22.5) * rad),
-        y1 = y + size * Math.cos((angle + 22.5) * rad),
-        dx = (x1 - x2) / 2,
-        dy = (y1 - y2) / 2,
-        rx = w / 2,
-        ry = h / 2,
-        k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
-        cx = k * rx * dy / ry + (x1 + x2) / 2,
-        cy = k * -ry * dx / rx + (y1 + y2) / 2;
-
-    p.attr({
-        x: cx,
-        y: cy,
-        path: [
-            "M", x, y,
-            "L", x1, y1,
-            "A", rx, ry, 0, 1, 1, x2, y2,
-            "z"
-        ].join(",")
-    });
-
-    this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2);
-
-    return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*
- * Tooltips on Paper prototype
- */
-/*\
- * Paper.label
- [ method ]
- **
- * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label
- **
- > Parameters
- **
- - x (number) x coordinate of the center of the label
- - y (number) y coordinate of the center of the label
- - text (string) text to place inside the label
- **
- = (object) set containing the label path and the text element
- > Usage
- | paper.label(50, 50, "$9.99");
- \*/
-Raphael.fn.label = function (x, y, text) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.label(), text);
-};
-
-/*\
- * Paper.popup
- [ method ]
- **
- * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup
- *
- * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively.
- **
- > Parameters
- **
- - x (number) x coordinate of the popup's tail
- - y (number) y coordinate of the popup's tail
- - text (string) text to place inside the popup
- - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
- - size (number) amount of padding around the Element [default: `5`]
- **
- = (object) set containing the popup path and the text element
- > Usage
- | paper.popup(50, 50, "$9.99", 'down');
- \*/
-Raphael.fn.popup = function (x, y, text, dir, size) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.popup(dir, size), text);
-};
-
-/*\
- * Paper.tag
- [ method ]
- **
- * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag
- **
- > Parameters
- **
- - x (number) x coordinate of the center of the tag loop
- - y (number) y coordinate of the center of the tag loop
- - text (string) text to place inside the tag
- - angle (number) angle of orientation in degrees [default: `0`]
- - r (number) radius of the loop [default: `5`]
- **
- = (object) set containing the tag path and the text element
- > Usage
- | paper.tag(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.tag = function (x, y, text, angle, r) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.tag(angle, r), text);
-};
-
-/*\
- * Paper.flag
- [ method ]
- **
- * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag
- **
- > Parameters
- **
- - x (number) x coordinate of the flag's point
- - y (number) y coordinate of the flag's point
- - text (string) text to place inside the flag
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the flag path and the text element
- > Usage
- | paper.flag(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.flag = function (x, y, text, angle) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.flag(angle), text);
-};
-
-/*\
- * Paper.drop
- [ method ]
- **
- * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop
- **
- > Parameters
- **
- - x (number) x coordinate of the drop's point
- - y (number) y coordinate of the drop's point
- - text (string) text to place inside the drop
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the drop path and the text element
- > Usage
- | paper.drop(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.drop = function (x, y, text, angle) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.drop(angle), text);
-};
-
-/*\
- * Paper.blob
- [ method ]
- **
- * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob
- **
- > Parameters
- **
- - x (number) x coordinate of the blob's tail
- - y (number) y coordinate of the blob's tail
- - text (string) text to place inside the blob
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the blob path and the text element
- > Usage
- | paper.blob(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.blob = function (x, y, text, angle) {
-    var set = this.set();
-
-    text = this.text(x, y, text).attr(Raphael.g.txtattr);
-    return set.push(text.blob(angle), text);
-};
-
-/**
- * Brightness functions on the Element prototype
- */
-/*\
- * Element.lighter
- [ method ]
- **
- * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets.
- **
- > Parameters
- **
- - times (number) adjustment factor [default: `2`]
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- |     fill: "#ff0000",
- |     stroke: "#fff",
- |     "stroke-width": 2
- | }).lighter(6);
- \*/
-Raphael.el.lighter = function (times) {
-    times = times || 2;
-
-    var fs = [this.attrs.fill, this.attrs.stroke];
-
-    this.fs = this.fs || [fs[0], fs[1]];
-
-    fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
-    fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
-    fs[0].b = Math.min(fs[0].b * times, 1);
-    fs[0].s = fs[0].s / times;
-    fs[1].b = Math.min(fs[1].b * times, 1);
-    fs[1].s = fs[1].s / times;
-
-    this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
-    return this;
-};
-
-/*\
- * Element.darker
- [ method ]
- **
- * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets.
- **
- > Parameters
- **
- - times (number) adjustment factor [default: `2`]
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- |     fill: "#ff0000",
- |     stroke: "#fff",
- |     "stroke-width": 2
- | }).darker(6);
- \*/
-Raphael.el.darker = function (times) {
-    times = times || 2;
-
-    var fs = [this.attrs.fill, this.attrs.stroke];
-
-    this.fs = this.fs || [fs[0], fs[1]];
-
-    fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
-    fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
-    fs[0].s = Math.min(fs[0].s * times, 1);
-    fs[0].b = fs[0].b / times;
-    fs[1].s = Math.min(fs[1].s * times, 1);
-    fs[1].b = fs[1].b / times;
-
-    this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
-    return this;
-};
-
-/*\
- * Element.resetBrightness
- [ method ]
- **
- * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets.
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- |     fill: "#ff0000",
- |     stroke: "#fff",
- |     "stroke-width": 2
- | }).lighter(6).resetBrightness();
- \*/
-Raphael.el.resetBrightness = function () {
-    if (this.fs) {
-        this.attr({ fill: this.fs[0], stroke: this.fs[1] });
-        delete this.fs;
-    }
-    return this;
-};
-
-//alias to set prototype
-(function () {
-    var brightness = ['lighter', 'darker', 'resetBrightness'],
-        tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob'];
-
-    for (var f in tooltips) (function (name) {
-        Raphael.st[name] = function () {
-            return Raphael.el[name].apply(this, arguments);
-        };
-    })(tooltips[f]);
-
-    for (var f in brightness) (function (name) {
-        Raphael.st[name] = function () {
-            for (var i = 0; i < this.length; i++) {
-                this[i][name].apply(this[i], arguments);
-            }
-
-            return this;
-        };
-    })(brightness[f]);
-})();
-
-//chart prototype for storing common functions
-Raphael.g = {
-    /*\
-     * g.shim
-     [ object ]
-     **
-     * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to)
-     **
-     > Default value
-     | { stroke: 'none', fill: '#000', 'fill-opacity': 0 }
-     \*/
-    shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 },
-
-    /*\
-     * g.txtattr
-     [ object ]
-     **
-     * An attribute object that charts and tooltips will set on any generated text
-     **
-     > Default value
-     | { font: '12px Arial, sans-serif', fill: '#fff' }
-     \*/  
-    txtattr: { font: '12px Arial, sans-serif', fill: '#fff' },
-
-    /*\
-     * g.colors
-     [ array ]
-     **
-     * An array of color values that charts will iterate through when drawing chart data values.
-     **
-     \*/
-    colors: (function () {
-            var hues = [.6, .2, .05, .1333, .75, 0],
-                colors = [];
-
-            for (var i = 0; i < 10; i++) {
-                if (i < hues.length) {
-                    colors.push('hsb(' + hues[i] + ',.75, .75)');
-                } else {
-                    colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)');
-                }
-            }
-
-            return colors;
-    })(),
-    
-    snapEnds: function(from, to, steps) {
-        var f = from,
-            t = to;
-
-        if (f == t) {
-            return {from: f, to: t, power: 0};
-        }
-
-        function round(a) {
-            return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a);
-        }
-
-        var d = (t - f) / steps,
-            r = ~~(d),
-            R = r,
-            i = 0;
-
-        if (r) {
-            while (R) {
-                i--;
-                R = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
-            }
-
-            i ++;
-        } else {
-            if(d == 0 || !isFinite(d)) {
-                i = 1;
-            } else {
-                while (!r) {
-                    i = i || 1;
-                    r = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
-                    i++;
-                }
-            }
-
-            i && i--;
-        }
-
-        t = round(to * Math.pow(10, i)) / Math.pow(10, i);
-
-        if (t < to) {
-            t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
-        }
-
-        f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
-        return { from: f, to: t, power: i };
-    },
-
-    axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) {
-        dashsize = dashsize == null ? 2 : dashsize;
-        type = type || "t";
-        steps = steps || 10;
-        paper = arguments[arguments.length-1] //paper is always last argument
-
-        var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
-            ends = this.snapEnds(from, to, steps),
-            f = ends.from,
-            t = ends.to,
-            i = ends.power,
-            j = 0,
-            txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" },
-            text = paper.set(),
-            d;
-
-        d = (t - f) / steps;
-
-        var label = f,
-            rnd = i > 0 ? i : 0;
-            dx = length / steps;
-
-        if (+orientation == 1 || +orientation == 3) {
-            var Y = y,
-                addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
-
-            while (Y >= y - length) {
-                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
-                text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
-                label += d;
-                Y -= dx;
-            }
-
-            if (Math.round(Y + dx - (y - length))) {
-                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
-                text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
-            }
-        } else {
-            label = f;
-            rnd = (i > 0) * i;
-            addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation);
-
-            var X = x,
-                dx = length / steps,
-                txt = 0,
-                prev = 0;
-
-            while (X <= x + length) {
-                type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
-                text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
-
-                var bb = txt.getBBox();
-
-                if (prev >= bb.x - 5) {
-                    text.pop(text.length - 1).remove();
-                } else {
-                    prev = bb.x + bb.width;
-                }
-
-                label += d;
-                X += dx;
-            }
-
-            if (Math.round(X - dx - x - length)) {
-                type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
-                text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
-            }
-        }
-
-        var res = paper.path(path);
-
-        res.text = text;
-        res.all = paper.set([res, text]);
-        res.remove = function () {
-            this.text.remove();
-            this.constructor.prototype.remove.call(this);
-        };
-
-        return res;
-    },
-    
-    labelise: function(label, val, total) {
-        if (label) {
-            return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
-                if (value) {
-                    return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
-                }
-                if (percent) {
-                    return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
-                }
-            });
-        } else {
-            return (+val).toFixed(0);
-        }
-    }
-}
diff --git a/vendor/assets/javascripts/jquery.highlight.js b/vendor/assets/javascripts/jquery.highlight.js
deleted file mode 100644
index 7a67cf99844921c9d93757a4da93ce7d84fe4664..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/jquery.highlight.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-
-highlight v3
-
-Highlights arbitrary terms.
-
-<http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html>
-
-MIT license.
-
-Johann Burkard
-<http://johannburkard.de>
-<mailto:jb@eaio.com>
-
-*/
-
-jQuery.fn.highlight = function(pat) {
- function innerHighlight(node, pat) {
-  var skip = 0;
-  if (node.nodeType == 3) {
-   var pos = node.data.toUpperCase().indexOf(pat);
-   if (pos >= 0) {
-    var spannode = document.createElement('span');
-    spannode.className = 'highlight_word';
-    var middlebit = node.splitText(pos);
-    var endbit = middlebit.splitText(pat.length);
-    var middleclone = middlebit.cloneNode(true);
-    spannode.appendChild(middleclone);
-    middlebit.parentNode.replaceChild(spannode, middlebit);
-    skip = 1;
-   }
-  }
-  else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
-   for (var i = 0; i < node.childNodes.length; ++i) {
-    i += innerHighlight(node.childNodes[i], pat);
-   }
-  }
-  return skip;
- }
- return this.each(function() {
-  innerHighlight(this, pat.toUpperCase());
- });
-};
-
-jQuery.fn.removeHighlight = function() {
- return this.find("span.highlight").each(function() {
-  this.parentNode.firstChild.nodeName;
-  with (this.parentNode) {
-   replaceChild(this.firstChild, this);
-   normalize();
-  }
- }).end();
-};
diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js
deleted file mode 100644
index 3f3f8a0b7f63af994df8a5e24afc521105296ba1..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/raphael.js
+++ /dev/null
@@ -1,8239 +0,0 @@
-// ┌────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël 2.1.4 - JavaScript Vector Library                          │ \\
-// ├────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com)    │ \\
-// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com)              │ \\
-// ├────────────────────────────────────────────────────────────────────┤ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\
-// └────────────────────────────────────────────────────────────────────┘ \\
-// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
-// 
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// 
-// http://www.apache.org/licenses/LICENSE-2.0
-// 
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-// ┌────────────────────────────────────────────────────────────┐ \\
-// │ Eve 0.4.2 - JavaScript Events Library                      │ \\
-// ├────────────────────────────────────────────────────────────┤ \\
-// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\
-// └────────────────────────────────────────────────────────────┘ \\
-
-(function (glob) {
-    var version = "0.4.2",
-        has = "hasOwnProperty",
-        separator = /[\.\/]/,
-        wildcard = "*",
-        fun = function () {},
-        numsort = function (a, b) {
-            return a - b;
-        },
-        current_event,
-        stop,
-        events = {n: {}},
-    /*\
-     * eve
-     [ method ]
-
-     * Fires event with given `name`, given scope and other parameters.
-
-     > Arguments
-
-     - name (string) name of the *event*, dot (`.`) or slash (`/`) separated
-     - scope (object) context for the event handlers
-     - varargs (...) the rest of arguments will be sent to event handlers
-
-     = (object) array of returned values from the listeners
-    \*/
-        eve = function (name, scope) {
-			name = String(name);
-            var e = events,
-                oldstop = stop,
-                args = Array.prototype.slice.call(arguments, 2),
-                listeners = eve.listeners(name),
-                z = 0,
-                f = false,
-                l,
-                indexed = [],
-                queue = {},
-                out = [],
-                ce = current_event,
-                errors = [];
-            current_event = name;
-            stop = 0;
-            for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) {
-                indexed.push(listeners[i].zIndex);
-                if (listeners[i].zIndex < 0) {
-                    queue[listeners[i].zIndex] = listeners[i];
-                }
-            }
-            indexed.sort(numsort);
-            while (indexed[z] < 0) {
-                l = queue[indexed[z++]];
-                out.push(l.apply(scope, args));
-                if (stop) {
-                    stop = oldstop;
-                    return out;
-                }
-            }
-            for (i = 0; i < ii; i++) {
-                l = listeners[i];
-                if ("zIndex" in l) {
-                    if (l.zIndex == indexed[z]) {
-                        out.push(l.apply(scope, args));
-                        if (stop) {
-                            break;
-                        }
-                        do {
-                            z++;
-                            l = queue[indexed[z]];
-                            l && out.push(l.apply(scope, args));
-                            if (stop) {
-                                break;
-                            }
-                        } while (l)
-                    } else {
-                        queue[l.zIndex] = l;
-                    }
-                } else {
-                    out.push(l.apply(scope, args));
-                    if (stop) {
-                        break;
-                    }
-                }
-            }
-            stop = oldstop;
-            current_event = ce;
-            return out.length ? out : null;
-        };
-		// Undocumented. Debug only.
-		eve._events = events;
-    /*\
-     * eve.listeners
-     [ method ]
-
-     * Internal method which gives you array of all event handlers that will be triggered by the given `name`.
-
-     > Arguments
-
-     - name (string) name of the event, dot (`.`) or slash (`/`) separated
-
-     = (array) array of event handlers
-    \*/
-    eve.listeners = function (name) {
-        var names = name.split(separator),
-            e = events,
-            item,
-            items,
-            k,
-            i,
-            ii,
-            j,
-            jj,
-            nes,
-            es = [e],
-            out = [];
-        for (i = 0, ii = names.length; i < ii; i++) {
-            nes = [];
-            for (j = 0, jj = es.length; j < jj; j++) {
-                e = es[j].n;
-                items = [e[names[i]], e[wildcard]];
-                k = 2;
-                while (k--) {
-                    item = items[k];
-                    if (item) {
-                        nes.push(item);
-                        out = out.concat(item.f || []);
-                    }
-                }
-            }
-            es = nes;
-        }
-        return out;
-    };
-    
-    /*\
-     * eve.on
-     [ method ]
-     **
-     * Binds given event handler with a given name. You can use wildcards “`*`” for the names:
-     | eve.on("*.under.*", f);
-     | eve("mouse.under.floor"); // triggers f
-     * Use @eve to trigger the listener.
-     **
-     > Arguments
-     **
-     - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
-     - f (function) event handler function
-     **
-     = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment. 
-     > Example:
-     | eve.on("mouse", eatIt)(2);
-     | eve.on("mouse", scream);
-     | eve.on("mouse", catchIt)(1);
-     * This will ensure that `catchIt()` function will be called before `eatIt()`.
-	 *
-     * If you want to put your handler before non-indexed handlers, specify a negative value.
-     * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”.
-    \*/
-    eve.on = function (name, f) {
-		name = String(name);
-		if (typeof f != "function") {
-			return function () {};
-		}
-        var names = name.split(separator),
-            e = events;
-        for (var i = 0, ii = names.length; i < ii; i++) {
-            e = e.n;
-            e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}});
-        }
-        e.f = e.f || [];
-        for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) {
-            return fun;
-        }
-        e.f.push(f);
-        return function (zIndex) {
-            if (+zIndex == +zIndex) {
-                f.zIndex = +zIndex;
-            }
-        };
-    };
-    /*\
-     * eve.f
-     [ method ]
-     **
-     * Returns function that will fire given event with optional arguments.
-	 * Arguments that will be passed to the result function will be also
-	 * concated to the list of final arguments.
- 	 | el.onclick = eve.f("click", 1, 2);
- 	 | eve.on("click", function (a, b, c) {
- 	 |     console.log(a, b, c); // 1, 2, [event object]
- 	 | });
-     > Arguments
-	 - event (string) event name
-	 - varargs (…) and any other arguments
-	 = (function) possible event handler function
-    \*/
-	eve.f = function (event) {
-		var attrs = [].slice.call(arguments, 1);
-		return function () {
-			eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0)));
-		};
-	};
-    /*\
-     * eve.stop
-     [ method ]
-     **
-     * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing.
-    \*/
-    eve.stop = function () {
-        stop = 1;
-    };
-    /*\
-     * eve.nt
-     [ method ]
-     **
-     * Could be used inside event handler to figure out actual name of the event.
-     **
-     > Arguments
-     **
-     - subname (string) #optional subname of the event
-     **
-     = (string) name of the event, if `subname` is not specified
-     * or
-     = (boolean) `true`, if current event’s name contains `subname`
-    \*/
-    eve.nt = function (subname) {
-        if (subname) {
-            return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event);
-        }
-        return current_event;
-    };
-    /*\
-     * eve.nts
-     [ method ]
-     **
-     * Could be used inside event handler to figure out actual name of the event.
-     **
-     **
-     = (array) names of the event
-    \*/
-    eve.nts = function () {
-        return current_event.split(separator);
-    };
-    /*\
-     * eve.off
-     [ method ]
-     **
-     * Removes given function from the list of event listeners assigned to given name.
-	 * If no arguments specified all the events will be cleared.
-     **
-     > Arguments
-     **
-     - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
-     - f (function) event handler function
-    \*/
-    /*\
-     * eve.unbind
-     [ method ]
-     **
-     * See @eve.off
-    \*/
-    eve.off = eve.unbind = function (name, f) {
-		if (!name) {
-		    eve._events = events = {n: {}};
-			return;
-		}
-        var names = name.split(separator),
-            e,
-            key,
-            splice,
-            i, ii, j, jj,
-            cur = [events];
-        for (i = 0, ii = names.length; i < ii; i++) {
-            for (j = 0; j < cur.length; j += splice.length - 2) {
-                splice = [j, 1];
-                e = cur[j].n;
-                if (names[i] != wildcard) {
-                    if (e[names[i]]) {
-                        splice.push(e[names[i]]);
-                    }
-                } else {
-                    for (key in e) if (e[has](key)) {
-                        splice.push(e[key]);
-                    }
-                }
-                cur.splice.apply(cur, splice);
-            }
-        }
-        for (i = 0, ii = cur.length; i < ii; i++) {
-            e = cur[i];
-            while (e.n) {
-                if (f) {
-                    if (e.f) {
-                        for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) {
-                            e.f.splice(j, 1);
-                            break;
-                        }
-                        !e.f.length && delete e.f;
-                    }
-                    for (key in e.n) if (e.n[has](key) && e.n[key].f) {
-                        var funcs = e.n[key].f;
-                        for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) {
-                            funcs.splice(j, 1);
-                            break;
-                        }
-                        !funcs.length && delete e.n[key].f;
-                    }
-                } else {
-                    delete e.f;
-                    for (key in e.n) if (e.n[has](key) && e.n[key].f) {
-                        delete e.n[key].f;
-                    }
-                }
-                e = e.n;
-            }
-        }
-    };
-    /*\
-     * eve.once
-     [ method ]
-     **
-     * Binds given event handler with a given name to only run once then unbind itself.
-     | eve.once("login", f);
-     | eve("login"); // triggers f
-     | eve("login"); // no listeners
-     * Use @eve to trigger the listener.
-     **
-     > Arguments
-     **
-     - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
-     - f (function) event handler function
-     **
-     = (function) same return function as @eve.on
-    \*/
-    eve.once = function (name, f) {
-        var f2 = function () {
-            eve.unbind(name, f2);
-            return f.apply(this, arguments);
-        };
-        return eve.on(name, f2);
-    };
-    /*\
-     * eve.version
-     [ property (string) ]
-     **
-     * Current version of the library.
-    \*/
-    eve.version = version;
-    eve.toString = function () {
-        return "You are running Eve " + version;
-    };
-    (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve));
-})(window || this);
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ "Raphaël 2.1.2" - JavaScript Vector Library                         │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com)   │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com)             │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function (glob, factory) {
-    // AMD support
-    if (typeof define === "function" && define.amd) {
-        // Define as an anonymous module
-        define(["eve"], function( eve ) {
-            return factory(glob, eve);
-        });
-    } else {
-        // Browser globals (glob is window)
-        // Raphael adds itself to window
-        factory(glob, glob.eve || (typeof require == "function" && require('eve')) );
-    }
-}(this, function (window, eve) {
-    /*\
-     * Raphael
-     [ method ]
-     **
-     * Creates a canvas object on which to draw.
-     * You must do this first, as all future calls to drawing methods
-     * from this instance will be bound to this canvas.
-     > Parameters
-     **
-     - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface
-     - width (number)
-     - height (number)
-     - callback (function) #optional callback function which is going to be executed in the context of newly created paper
-     * or
-     - x (number)
-     - y (number)
-     - width (number)
-     - height (number)
-     - callback (function) #optional callback function which is going to be executed in the context of newly created paper
-     * or
-     - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add.
-     - callback (function) #optional callback function which is going to be executed in the context of newly created paper
-     * or
-     - onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`.
-     = (object) @Paper
-     > Usage
-     | // Each of the following examples create a canvas
-     | // that is 320px wide by 200px high.
-     | // Canvas is created at the viewport’s 10,50 coordinate.
-     | var paper = Raphael(10, 50, 320, 200);
-     | // Canvas is created at the top left corner of the #notepad element
-     | // (or its top right corner in dir="rtl" elements)
-     | var paper = Raphael(document.getElementById("notepad"), 320, 200);
-     | // Same as above
-     | var paper = Raphael("notepad", 320, 200);
-     | // Image dump
-     | var set = Raphael(["notepad", 320, 200, {
-     |     type: "rect",
-     |     x: 10,
-     |     y: 10,
-     |     width: 25,
-     |     height: 25,
-     |     stroke: "#f00"
-     | }, {
-     |     type: "text",
-     |     x: 30,
-     |     y: 40,
-     |     text: "Dump"
-     | }]);
-    \*/
-    function R(first) {
-        if (R.is(first, "function")) {
-            return loaded ? first() : eve.on("raphael.DOMload", first);
-        } else if (R.is(first, array)) {
-            return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first);
-        } else {
-            var args = Array.prototype.slice.call(arguments, 0);
-            if (R.is(args[args.length - 1], "function")) {
-                var f = args.pop();
-                return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () {
-                    f.call(R._engine.create[apply](R, args));
-                });
-            } else {
-                return R._engine.create[apply](R, arguments);
-            }
-        }
-    }
-    R.version = "2.1.2";
-    R.eve = eve;
-    var loaded,
-        separator = /[, ]+/,
-        elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1},
-        formatrg = /\{(\d+)\}/g,
-        proto = "prototype",
-        has = "hasOwnProperty",
-        g = {
-            doc: document,
-            win: window
-        },
-        oldRaphael = {
-            was: Object.prototype[has].call(g.win, "Raphael"),
-            is: g.win.Raphael
-        },
-        Paper = function () {
-            /*\
-             * Paper.ca
-             [ property (object) ]
-             **
-             * Shortcut for @Paper.customAttributes
-            \*/
-            /*\
-             * Paper.customAttributes
-             [ property (object) ]
-             **
-             * If you have a set of attributes that you would like to represent
-             * as a function of some number you can do it easily with custom attributes:
-             > Usage
-             | paper.customAttributes.hue = function (num) {
-             |     num = num % 1;
-             |     return {fill: "hsb(" + num + ", 0.75, 1)"};
-             | };
-             | // Custom attribute “hue” will change fill
-             | // to be given hue with fixed saturation and brightness.
-             | // Now you can use it like this:
-             | var c = paper.circle(10, 10, 10).attr({hue: .45});
-             | // or even like this:
-             | c.animate({hue: 1}, 1e3);
-             |
-             | // You could also create custom attribute
-             | // with multiple parameters:
-             | paper.customAttributes.hsb = function (h, s, b) {
-             |     return {fill: "hsb(" + [h, s, b].join(",") + ")"};
-             | };
-             | c.attr({hsb: "0.5 .8 1"});
-             | c.animate({hsb: [1, 0, 0.5]}, 1e3);
-            \*/
-            this.ca = this.customAttributes = {};
-        },
-        paperproto,
-        appendChild = "appendChild",
-        apply = "apply",
-        concat = "concat",
-        supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test
-        E = "",
-        S = " ",
-        Str = String,
-        split = "split",
-        events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S),
-        touchMap = {
-            mousedown: "touchstart",
-            mousemove: "touchmove",
-            mouseup: "touchend"
-        },
-        lowerCase = Str.prototype.toLowerCase,
-        math = Math,
-        mmax = math.max,
-        mmin = math.min,
-        abs = math.abs,
-        pow = math.pow,
-        PI = math.PI,
-        nu = "number",
-        string = "string",
-        array = "array",
-        toString = "toString",
-        fillString = "fill",
-        objectToString = Object.prototype.toString,
-        paper = {},
-        push = "push",
-        ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i,
-        colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i,
-        isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1},
-        bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
-        round = math.round,
-        setAttribute = "setAttribute",
-        toFloat = parseFloat,
-        toInt = parseInt,
-        upperCase = Str.prototype.toUpperCase,
-        availableAttrs = R._availableAttrs = {
-            "arrow-end": "none",
-            "arrow-start": "none",
-            blur: 0,
-            "clip-rect": "0 0 1e9 1e9",
-            cursor: "default",
-            cx: 0,
-            cy: 0,
-            fill: "#fff",
-            "fill-opacity": 1,
-            font: '10px "Arial"',
-            "font-family": '"Arial"',
-            "font-size": "10",
-            "font-style": "normal",
-            "font-weight": 400,
-            gradient: 0,
-            height: 0,
-            href: "http://raphaeljs.com/",
-            "letter-spacing": 0,
-            opacity: 1,
-            path: "M0,0",
-            r: 0,
-            rx: 0,
-            ry: 0,
-            src: "",
-            stroke: "#000",
-            "stroke-dasharray": "",
-            "stroke-linecap": "butt",
-            "stroke-linejoin": "butt",
-            "stroke-miterlimit": 0,
-            "stroke-opacity": 1,
-            "stroke-width": 1,
-            target: "_blank",
-            "text-anchor": "middle",
-            title: "Raphael",
-            transform: "",
-            width: 0,
-            x: 0,
-            y: 0
-        },
-        availableAnimAttrs = R._availableAnimAttrs = {
-            blur: nu,
-            "clip-rect": "csv",
-            cx: nu,
-            cy: nu,
-            fill: "colour",
-            "fill-opacity": nu,
-            "font-size": nu,
-            height: nu,
-            opacity: nu,
-            path: "path",
-            r: nu,
-            rx: nu,
-            ry: nu,
-            stroke: "colour",
-            "stroke-opacity": nu,
-            "stroke-width": nu,
-            transform: "transform",
-            width: nu,
-            x: nu,
-            y: nu
-        },
-        whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g,
-        commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/,
-        hsrg = {hs: 1, rg: 1},
-        p2s = /,?([achlmqrstvxz]),?/gi,
-        pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
-        tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
-        pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig,
-        radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/,
-        eldata = {},
-        sortByKey = function (a, b) {
-            return a.key - b.key;
-        },
-        sortByNumber = function (a, b) {
-            return toFloat(a) - toFloat(b);
-        },
-        fun = function () {},
-        pipe = function (x) {
-            return x;
-        },
-        rectPath = R._rectPath = function (x, y, w, h, r) {
-            if (r) {
-                return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
-            }
-            return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
-        },
-        ellipsePath = function (x, y, rx, ry) {
-            if (ry == null) {
-                ry = rx;
-            }
-            return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
-        },
-        getPath = R._getPath = {
-            path: function (el) {
-                return el.attr("path");
-            },
-            circle: function (el) {
-                var a = el.attrs;
-                return ellipsePath(a.cx, a.cy, a.r);
-            },
-            ellipse: function (el) {
-                var a = el.attrs;
-                return ellipsePath(a.cx, a.cy, a.rx, a.ry);
-            },
-            rect: function (el) {
-                var a = el.attrs;
-                return rectPath(a.x, a.y, a.width, a.height, a.r);
-            },
-            image: function (el) {
-                var a = el.attrs;
-                return rectPath(a.x, a.y, a.width, a.height);
-            },
-            text: function (el) {
-                var bbox = el._getBBox();
-                return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
-            },
-            set : function(el) {
-                var bbox = el._getBBox();
-                return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
-            }
-        },
-        /*\
-         * Raphael.mapPath
-         [ method ]
-         **
-         * Transform the path string with given matrix.
-         > Parameters
-         - path (string) path string
-         - matrix (object) see @Matrix
-         = (string) transformed path string
-        \*/
-        mapPath = R.mapPath = function (path, matrix) {
-            if (!matrix) {
-                return path;
-            }
-            var x, y, i, j, ii, jj, pathi;
-            path = path2curve(path);
-            for (i = 0, ii = path.length; i < ii; i++) {
-                pathi = path[i];
-                for (j = 1, jj = pathi.length; j < jj; j += 2) {
-                    x = matrix.x(pathi[j], pathi[j + 1]);
-                    y = matrix.y(pathi[j], pathi[j + 1]);
-                    pathi[j] = x;
-                    pathi[j + 1] = y;
-                }
-            }
-            return path;
-        };
-
-    R._g = g;
-    /*\
-     * Raphael.type
-     [ property (string) ]
-     **
-     * Can be “SVG”, “VML” or empty, depending on browser support.
-    \*/
-    R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
-    if (R.type == "VML") {
-        var d = g.doc.createElement("div"),
-            b;
-        d.innerHTML = '<v:shape adj="1"/>';
-        b = d.firstChild;
-        b.style.behavior = "url(#default#VML)";
-        if (!(b && typeof b.adj == "object")) {
-            return (R.type = E);
-        }
-        d = null;
-    }
-    /*\
-     * Raphael.svg
-     [ property (boolean) ]
-     **
-     * `true` if browser supports SVG.
-    \*/
-    /*\
-     * Raphael.vml
-     [ property (boolean) ]
-     **
-     * `true` if browser supports VML.
-    \*/
-    R.svg = !(R.vml = R.type == "VML");
-    R._Paper = Paper;
-    /*\
-     * Raphael.fn
-     [ property (object) ]
-     **
-     * You can add your own method to the canvas. For example if you want to draw a pie chart,
-     * you can create your own pie chart function and ship it as a Raphaël plugin. To do this
-     * you need to extend the `Raphael.fn` object. You should modify the `fn` object before a
-     * Raphaël instance is created, otherwise it will take no effect. Please note that the
-     * ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to
-     * ensure any namespacing ensures proper context.
-     > Usage
-     | Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
-     |     return this.path( ... );
-     | };
-     | // or create namespace
-     | Raphael.fn.mystuff = {
-     |     arrow: function () {…},
-     |     star: function () {…},
-     |     // etc…
-     | };
-     | var paper = Raphael(10, 10, 630, 480);
-     | // then use it
-     | paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"});
-     | paper.mystuff.arrow();
-     | paper.mystuff.star();
-    \*/
-    R.fn = paperproto = Paper.prototype = R.prototype;
-    R._id = 0;
-    R._oid = 0;
-    /*\
-     * Raphael.is
-     [ method ]
-     **
-     * Handful of replacements for `typeof` operator.
-     > Parameters
-     - o (…) any object or primitive
-     - type (string) name of the type, i.e. “string”, “function”, “number”, etc.
-     = (boolean) is given value is of given type
-    \*/
-    R.is = function (o, type) {
-        type = lowerCase.call(type);
-        if (type == "finite") {
-            return !isnan[has](+o);
-        }
-        if (type == "array") {
-            return o instanceof Array;
-        }
-        return  (type == "null" && o === null) ||
-                (type == typeof o && o !== null) ||
-                (type == "object" && o === Object(o)) ||
-                (type == "array" && Array.isArray && Array.isArray(o)) ||
-                objectToString.call(o).slice(8, -1).toLowerCase() == type;
-    };
-
-    function clone(obj) {
-        if (typeof obj == "function" || Object(obj) !== obj) {
-            return obj;
-        }
-        var res = new obj.constructor;
-        for (var key in obj) if (obj[has](key)) {
-            res[key] = clone(obj[key]);
-        }
-        return res;
-    }
-
-    /*\
-     * Raphael.angle
-     [ method ]
-     **
-     * Returns angle between two or three points
-     > Parameters
-     - x1 (number) x coord of first point
-     - y1 (number) y coord of first point
-     - x2 (number) x coord of second point
-     - y2 (number) y coord of second point
-     - x3 (number) #optional x coord of third point
-     - y3 (number) #optional y coord of third point
-     = (number) angle in degrees.
-    \*/
-    R.angle = function (x1, y1, x2, y2, x3, y3) {
-        if (x3 == null) {
-            var x = x1 - x2,
-                y = y1 - y2;
-            if (!x && !y) {
-                return 0;
-            }
-            return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360;
-        } else {
-            return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3);
-        }
-    };
-    /*\
-     * Raphael.rad
-     [ method ]
-     **
-     * Transform angle to radians
-     > Parameters
-     - deg (number) angle in degrees
-     = (number) angle in radians.
-    \*/
-    R.rad = function (deg) {
-        return deg % 360 * PI / 180;
-    };
-    /*\
-     * Raphael.deg
-     [ method ]
-     **
-     * Transform angle to degrees
-     > Parameters
-     - rad (number) angle in radians
-     = (number) angle in degrees.
-    \*/
-    R.deg = function (rad) {
-        return Math.round ((rad * 180 / PI% 360)* 1000) / 1000;
-    };
-    /*\
-     * Raphael.snapTo
-     [ method ]
-     **
-     * Snaps given value to given grid.
-     > Parameters
-     - values (array|number) given array of values or step of the grid
-     - value (number) value to adjust
-     - tolerance (number) #optional tolerance for snapping. Default is `10`.
-     = (number) adjusted value.
-    \*/
-    R.snapTo = function (values, value, tolerance) {
-        tolerance = R.is(tolerance, "finite") ? tolerance : 10;
-        if (R.is(values, array)) {
-            var i = values.length;
-            while (i--) if (abs(values[i] - value) <= tolerance) {
-                return values[i];
-            }
-        } else {
-            values = +values;
-            var rem = value % values;
-            if (rem < tolerance) {
-                return value - rem;
-            }
-            if (rem > values - tolerance) {
-                return value - rem + values;
-            }
-        }
-        return value;
-    };
-
-    /*\
-     * Raphael.createUUID
-     [ method ]
-     **
-     * Returns RFC4122, version 4 ID
-    \*/
-    var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) {
-        return function () {
-            return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase();
-        };
-    })(/[xy]/g, function (c) {
-        var r = math.random() * 16 | 0,
-            v = c == "x" ? r : (r & 3 | 8);
-        return v.toString(16);
-    });
-
-    /*\
-     * Raphael.setWindow
-     [ method ]
-     **
-     * Used when you need to draw in `&lt;iframe>`. Switched window to the iframe one.
-     > Parameters
-     - newwin (window) new window object
-    \*/
-    R.setWindow = function (newwin) {
-        eve("raphael.setWindow", R, g.win, newwin);
-        g.win = newwin;
-        g.doc = g.win.document;
-        if (R._engine.initWin) {
-            R._engine.initWin(g.win);
-        }
-    };
-    var toHex = function (color) {
-        if (R.vml) {
-            // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/
-            var trim = /^\s+|\s+$/g;
-            var bod;
-            try {
-                var docum = new ActiveXObject("htmlfile");
-                docum.write("<body>");
-                docum.close();
-                bod = docum.body;
-            } catch(e) {
-                bod = createPopup().document.body;
-            }
-            var range = bod.createTextRange();
-            toHex = cacher(function (color) {
-                try {
-                    bod.style.color = Str(color).replace(trim, E);
-                    var value = range.queryCommandValue("ForeColor");
-                    value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16);
-                    return "#" + ("000000" + value.toString(16)).slice(-6);
-                } catch(e) {
-                    return "none";
-                }
-            });
-        } else {
-            var i = g.doc.createElement("i");
-            i.title = "Rapha\xebl Colour Picker";
-            i.style.display = "none";
-            g.doc.body.appendChild(i);
-            toHex = cacher(function (color) {
-                i.style.color = color;
-                return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color");
-            });
-        }
-        return toHex(color);
-    },
-    hsbtoString = function () {
-        return "hsb(" + [this.h, this.s, this.b] + ")";
-    },
-    hsltoString = function () {
-        return "hsl(" + [this.h, this.s, this.l] + ")";
-    },
-    rgbtoString = function () {
-        return this.hex;
-    },
-    prepareRGB = function (r, g, b) {
-        if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) {
-            b = r.b;
-            g = r.g;
-            r = r.r;
-        }
-        if (g == null && R.is(r, string)) {
-            var clr = R.getRGB(r);
-            r = clr.r;
-            g = clr.g;
-            b = clr.b;
-        }
-        if (r > 1 || g > 1 || b > 1) {
-            r /= 255;
-            g /= 255;
-            b /= 255;
-        }
-
-        return [r, g, b];
-    },
-    packageRGB = function (r, g, b, o) {
-        r *= 255;
-        g *= 255;
-        b *= 255;
-        var rgb = {
-            r: r,
-            g: g,
-            b: b,
-            hex: R.rgb(r, g, b),
-            toString: rgbtoString
-        };
-        R.is(o, "finite") && (rgb.opacity = o);
-        return rgb;
-    };
-
-    /*\
-     * Raphael.color
-     [ method ]
-     **
-     * Parses the color string and returns object with all values for the given color.
-     > Parameters
-     - clr (string) color string in one of the supported formats (see @Raphael.getRGB)
-     = (object) Combined RGB & HSB object in format:
-     o {
-     o     r (number) red,
-     o     g (number) green,
-     o     b (number) blue,
-     o     hex (string) color in HTML/CSS format: #••••••,
-     o     error (boolean) `true` if string can’t be parsed,
-     o     h (number) hue,
-     o     s (number) saturation,
-     o     v (number) value (brightness),
-     o     l (number) lightness
-     o }
-    \*/
-    R.color = function (clr) {
-        var rgb;
-        if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) {
-            rgb = R.hsb2rgb(clr);
-            clr.r = rgb.r;
-            clr.g = rgb.g;
-            clr.b = rgb.b;
-            clr.hex = rgb.hex;
-        } else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) {
-            rgb = R.hsl2rgb(clr);
-            clr.r = rgb.r;
-            clr.g = rgb.g;
-            clr.b = rgb.b;
-            clr.hex = rgb.hex;
-        } else {
-            if (R.is(clr, "string")) {
-                clr = R.getRGB(clr);
-            }
-            if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) {
-                rgb = R.rgb2hsl(clr);
-                clr.h = rgb.h;
-                clr.s = rgb.s;
-                clr.l = rgb.l;
-                rgb = R.rgb2hsb(clr);
-                clr.v = rgb.b;
-            } else {
-                clr = {hex: "none"};
-                clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1;
-            }
-        }
-        clr.toString = rgbtoString;
-        return clr;
-    };
-    /*\
-     * Raphael.hsb2rgb
-     [ method ]
-     **
-     * Converts HSB values to RGB object.
-     > Parameters
-     - h (number) hue
-     - s (number) saturation
-     - v (number) value or brightness
-     = (object) RGB object in format:
-     o {
-     o     r (number) red,
-     o     g (number) green,
-     o     b (number) blue,
-     o     hex (string) color in HTML/CSS format: #••••••
-     o }
-    \*/
-    R.hsb2rgb = function (h, s, v, o) {
-        if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) {
-            v = h.b;
-            s = h.s;
-            o = h.o;
-            h = h.h;
-        }
-        h *= 360;
-        var R, G, B, X, C;
-        h = (h % 360) / 60;
-        C = v * s;
-        X = C * (1 - abs(h % 2 - 1));
-        R = G = B = v - C;
-
-        h = ~~h;
-        R += [C, X, 0, 0, X, C][h];
-        G += [X, C, C, X, 0, 0][h];
-        B += [0, 0, X, C, C, X][h];
-        return packageRGB(R, G, B, o);
-    };
-    /*\
-     * Raphael.hsl2rgb
-     [ method ]
-     **
-     * Converts HSL values to RGB object.
-     > Parameters
-     - h (number) hue
-     - s (number) saturation
-     - l (number) luminosity
-     = (object) RGB object in format:
-     o {
-     o     r (number) red,
-     o     g (number) green,
-     o     b (number) blue,
-     o     hex (string) color in HTML/CSS format: #••••••
-     o }
-    \*/
-    R.hsl2rgb = function (h, s, l, o) {
-        if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) {
-            l = h.l;
-            s = h.s;
-            h = h.h;
-        }
-        if (h > 1 || s > 1 || l > 1) {
-            h /= 360;
-            s /= 100;
-            l /= 100;
-        }
-        h *= 360;
-        var R, G, B, X, C;
-        h = (h % 360) / 60;
-        C = 2 * s * (l < .5 ? l : 1 - l);
-        X = C * (1 - abs(h % 2 - 1));
-        R = G = B = l - C / 2;
-
-        h = ~~h;
-        R += [C, X, 0, 0, X, C][h];
-        G += [X, C, C, X, 0, 0][h];
-        B += [0, 0, X, C, C, X][h];
-        return packageRGB(R, G, B, o);
-    };
-    /*\
-     * Raphael.rgb2hsb
-     [ method ]
-     **
-     * Converts RGB values to HSB object.
-     > Parameters
-     - r (number) red
-     - g (number) green
-     - b (number) blue
-     = (object) HSB object in format:
-     o {
-     o     h (number) hue
-     o     s (number) saturation
-     o     b (number) brightness
-     o }
-    \*/
-    R.rgb2hsb = function (r, g, b) {
-        b = prepareRGB(r, g, b);
-        r = b[0];
-        g = b[1];
-        b = b[2];
-
-        var H, S, V, C;
-        V = mmax(r, g, b);
-        C = V - mmin(r, g, b);
-        H = (C == 0 ? null :
-             V == r ? (g - b) / C :
-             V == g ? (b - r) / C + 2 :
-                      (r - g) / C + 4
-            );
-        H = ((H + 360) % 6) * 60 / 360;
-        S = C == 0 ? 0 : C / V;
-        return {h: H, s: S, b: V, toString: hsbtoString};
-    };
-    /*\
-     * Raphael.rgb2hsl
-     [ method ]
-     **
-     * Converts RGB values to HSL object.
-     > Parameters
-     - r (number) red
-     - g (number) green
-     - b (number) blue
-     = (object) HSL object in format:
-     o {
-     o     h (number) hue
-     o     s (number) saturation
-     o     l (number) luminosity
-     o }
-    \*/
-    R.rgb2hsl = function (r, g, b) {
-        b = prepareRGB(r, g, b);
-        r = b[0];
-        g = b[1];
-        b = b[2];
-
-        var H, S, L, M, m, C;
-        M = mmax(r, g, b);
-        m = mmin(r, g, b);
-        C = M - m;
-        H = (C == 0 ? null :
-             M == r ? (g - b) / C :
-             M == g ? (b - r) / C + 2 :
-                      (r - g) / C + 4);
-        H = ((H + 360) % 6) * 60 / 360;
-        L = (M + m) / 2;
-        S = (C == 0 ? 0 :
-             L < .5 ? C / (2 * L) :
-                      C / (2 - 2 * L));
-        return {h: H, s: S, l: L, toString: hsltoString};
-    };
-    R._path2string = function () {
-        return this.join(",").replace(p2s, "$1");
-    };
-    function repush(array, item) {
-        for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) {
-            return array.push(array.splice(i, 1)[0]);
-        }
-    }
-    function cacher(f, scope, postprocessor) {
-        function newf() {
-            var arg = Array.prototype.slice.call(arguments, 0),
-                args = arg.join("\u2400"),
-                cache = newf.cache = newf.cache || {},
-                count = newf.count = newf.count || [];
-            if (cache[has](args)) {
-                repush(count, args);
-                return postprocessor ? postprocessor(cache[args]) : cache[args];
-            }
-            count.length >= 1e3 && delete cache[count.shift()];
-            count.push(args);
-            cache[args] = f[apply](scope, arg);
-            return postprocessor ? postprocessor(cache[args]) : cache[args];
-        }
-        return newf;
-    }
-
-    var preload = R._preload = function (src, f) {
-        var img = g.doc.createElement("img");
-        img.style.cssText = "position:absolute;left:-9999em;top:-9999em";
-        img.onload = function () {
-            f.call(this);
-            this.onload = null;
-            g.doc.body.removeChild(this);
-        };
-        img.onerror = function () {
-            g.doc.body.removeChild(this);
-        };
-        g.doc.body.appendChild(img);
-        img.src = src;
-    };
-
-    function clrToString() {
-        return this.hex;
-    }
-
-    /*\
-     * Raphael.getRGB
-     [ method ]
-     **
-     * Parses colour string as RGB object
-     > Parameters
-     - colour (string) colour string in one of formats:
-     # <ul>
-     #     <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
-     #     <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
-     #     <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
-     #     <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
-     #     <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
-     #     <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
-     #     <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
-     #     <li>hsl(•••, •••, •••) — same as hsb</li>
-     #     <li>hsl(•••%, •••%, •••%) — same as hsb</li>
-     # </ul>
-     = (object) RGB object in format:
-     o {
-     o     r (number) red,
-     o     g (number) green,
-     o     b (number) blue
-     o     hex (string) color in HTML/CSS format: #••••••,
-     o     error (boolean) true if string can’t be parsed
-     o }
-    \*/
-    R.getRGB = cacher(function (colour) {
-        if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) {
-            return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
-        }
-        if (colour == "none") {
-            return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString};
-        }
-        !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour));
-        var res,
-            red,
-            green,
-            blue,
-            opacity,
-            t,
-            values,
-            rgb = colour.match(colourRegExp);
-        if (rgb) {
-            if (rgb[2]) {
-                blue = toInt(rgb[2].substring(5), 16);
-                green = toInt(rgb[2].substring(3, 5), 16);
-                red = toInt(rgb[2].substring(1, 3), 16);
-            }
-            if (rgb[3]) {
-                blue = toInt((t = rgb[3].charAt(3)) + t, 16);
-                green = toInt((t = rgb[3].charAt(2)) + t, 16);
-                red = toInt((t = rgb[3].charAt(1)) + t, 16);
-            }
-            if (rgb[4]) {
-                values = rgb[4][split](commaSpaces);
-                red = toFloat(values[0]);
-                values[0].slice(-1) == "%" && (red *= 2.55);
-                green = toFloat(values[1]);
-                values[1].slice(-1) == "%" && (green *= 2.55);
-                blue = toFloat(values[2]);
-                values[2].slice(-1) == "%" && (blue *= 2.55);
-                rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3]));
-                values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
-            }
-            if (rgb[5]) {
-                values = rgb[5][split](commaSpaces);
-                red = toFloat(values[0]);
-                values[0].slice(-1) == "%" && (red *= 2.55);
-                green = toFloat(values[1]);
-                values[1].slice(-1) == "%" && (green *= 2.55);
-                blue = toFloat(values[2]);
-                values[2].slice(-1) == "%" && (blue *= 2.55);
-                (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
-                rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3]));
-                values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
-                return R.hsb2rgb(red, green, blue, opacity);
-            }
-            if (rgb[6]) {
-                values = rgb[6][split](commaSpaces);
-                red = toFloat(values[0]);
-                values[0].slice(-1) == "%" && (red *= 2.55);
-                green = toFloat(values[1]);
-                values[1].slice(-1) == "%" && (green *= 2.55);
-                blue = toFloat(values[2]);
-                values[2].slice(-1) == "%" && (blue *= 2.55);
-                (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
-                rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3]));
-                values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
-                return R.hsl2rgb(red, green, blue, opacity);
-            }
-            rgb = {r: red, g: green, b: blue, toString: clrToString};
-            rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1);
-            R.is(opacity, "finite") && (rgb.opacity = opacity);
-            return rgb;
-        }
-        return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
-    }, R);
-    /*\
-     * Raphael.hsb
-     [ method ]
-     **
-     * Converts HSB values to hex representation of the colour.
-     > Parameters
-     - h (number) hue
-     - s (number) saturation
-     - b (number) value or brightness
-     = (string) hex representation of the colour.
-    \*/
-    R.hsb = cacher(function (h, s, b) {
-        return R.hsb2rgb(h, s, b).hex;
-    });
-    /*\
-     * Raphael.hsl
-     [ method ]
-     **
-     * Converts HSL values to hex representation of the colour.
-     > Parameters
-     - h (number) hue
-     - s (number) saturation
-     - l (number) luminosity
-     = (string) hex representation of the colour.
-    \*/
-    R.hsl = cacher(function (h, s, l) {
-        return R.hsl2rgb(h, s, l).hex;
-    });
-    /*\
-     * Raphael.rgb
-     [ method ]
-     **
-     * Converts RGB values to hex representation of the colour.
-     > Parameters
-     - r (number) red
-     - g (number) green
-     - b (number) blue
-     = (string) hex representation of the colour.
-    \*/
-    R.rgb = cacher(function (r, g, b) {
-        return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1);
-    });
-    /*\
-     * Raphael.getColor
-     [ method ]
-     **
-     * On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset
-     > Parameters
-     - value (number) #optional brightness, default is `0.75`
-     = (string) hex representation of the colour.
-    \*/
-    R.getColor = function (value) {
-        var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75},
-            rgb = this.hsb2rgb(start.h, start.s, start.b);
-        start.h += .075;
-        if (start.h > 1) {
-            start.h = 0;
-            start.s -= .2;
-            start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b});
-        }
-        return rgb.hex;
-    };
-    /*\
-     * Raphael.getColor.reset
-     [ method ]
-     **
-     * Resets spectrum position for @Raphael.getColor back to red.
-    \*/
-    R.getColor.reset = function () {
-        delete this.start;
-    };
-
-    // http://schepers.cc/getting-to-the-point
-    function catmullRom2bezier(crp, z) {
-        var d = [];
-        for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) {
-            var p = [
-                        {x: +crp[i - 2], y: +crp[i - 1]},
-                        {x: +crp[i],     y: +crp[i + 1]},
-                        {x: +crp[i + 2], y: +crp[i + 3]},
-                        {x: +crp[i + 4], y: +crp[i + 5]}
-                    ];
-            if (z) {
-                if (!i) {
-                    p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]};
-                } else if (iLen - 4 == i) {
-                    p[3] = {x: +crp[0], y: +crp[1]};
-                } else if (iLen - 2 == i) {
-                    p[2] = {x: +crp[0], y: +crp[1]};
-                    p[3] = {x: +crp[2], y: +crp[3]};
-                }
-            } else {
-                if (iLen - 4 == i) {
-                    p[3] = p[2];
-                } else if (!i) {
-                    p[0] = {x: +crp[i], y: +crp[i + 1]};
-                }
-            }
-            d.push(["C",
-                  (-p[0].x + 6 * p[1].x + p[2].x) / 6,
-                  (-p[0].y + 6 * p[1].y + p[2].y) / 6,
-                  (p[1].x + 6 * p[2].x - p[3].x) / 6,
-                  (p[1].y + 6*p[2].y - p[3].y) / 6,
-                  p[2].x,
-                  p[2].y
-            ]);
-        }
-
-        return d;
-    }
-    /*\
-     * Raphael.parsePathString
-     [ method ]
-     **
-     * Utility method
-     **
-     * Parses given path string into an array of arrays of path segments.
-     > Parameters
-     - pathString (string|array) path string or array of segments (in the last case it will be returned straight away)
-     = (array) array of segments.
-    \*/
-    R.parsePathString = function (pathString) {
-        if (!pathString) {
-            return null;
-        }
-        var pth = paths(pathString);
-        if (pth.arr) {
-            return pathClone(pth.arr);
-        }
-
-        var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0},
-            data = [];
-        if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption
-            data = pathClone(pathString);
-        }
-        if (!data.length) {
-            Str(pathString).replace(pathCommand, function (a, b, c) {
-                var params = [],
-                    name = b.toLowerCase();
-                c.replace(pathValues, function (a, b) {
-                    b && params.push(+b);
-                });
-                if (name == "m" && params.length > 2) {
-                    data.push([b][concat](params.splice(0, 2)));
-                    name = "l";
-                    b = b == "m" ? "l" : "L";
-                }
-                if (name == "r") {
-                    data.push([b][concat](params));
-                } else while (params.length >= paramCounts[name]) {
-                    data.push([b][concat](params.splice(0, paramCounts[name])));
-                    if (!paramCounts[name]) {
-                        break;
-                    }
-                }
-            });
-        }
-        data.toString = R._path2string;
-        pth.arr = pathClone(data);
-        return data;
-    };
-    /*\
-     * Raphael.parseTransformString
-     [ method ]
-     **
-     * Utility method
-     **
-     * Parses given path string into an array of transformations.
-     > Parameters
-     - TString (string|array) transform string or array of transformations (in the last case it will be returned straight away)
-     = (array) array of transformations.
-    \*/
-    R.parseTransformString = cacher(function (TString) {
-        if (!TString) {
-            return null;
-        }
-        var paramCounts = {r: 3, s: 4, t: 2, m: 6},
-            data = [];
-        if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption
-            data = pathClone(TString);
-        }
-        if (!data.length) {
-            Str(TString).replace(tCommand, function (a, b, c) {
-                var params = [],
-                    name = lowerCase.call(b);
-                c.replace(pathValues, function (a, b) {
-                    b && params.push(+b);
-                });
-                data.push([b][concat](params));
-            });
-        }
-        data.toString = R._path2string;
-        return data;
-    });
-    // PATHS
-    var paths = function (ps) {
-        var p = paths.ps = paths.ps || {};
-        if (p[ps]) {
-            p[ps].sleep = 100;
-        } else {
-            p[ps] = {
-                sleep: 100
-            };
-        }
-        setTimeout(function () {
-            for (var key in p) if (p[has](key) && key != ps) {
-                p[key].sleep--;
-                !p[key].sleep && delete p[key];
-            }
-        });
-        return p[ps];
-    };
-    /*\
-     * Raphael.findDotsAtSegment
-     [ method ]
-     **
-     * Utility method
-     **
-     * Find dot coordinates on the given cubic bezier curve at the given t.
-     > Parameters
-     - p1x (number) x of the first point of the curve
-     - p1y (number) y of the first point of the curve
-     - c1x (number) x of the first anchor of the curve
-     - c1y (number) y of the first anchor of the curve
-     - c2x (number) x of the second anchor of the curve
-     - c2y (number) y of the second anchor of the curve
-     - p2x (number) x of the second point of the curve
-     - p2y (number) y of the second point of the curve
-     - t (number) position on the curve (0..1)
-     = (object) point information in format:
-     o {
-     o     x: (number) x coordinate of the point
-     o     y: (number) y coordinate of the point
-     o     m: {
-     o         x: (number) x coordinate of the left anchor
-     o         y: (number) y coordinate of the left anchor
-     o     }
-     o     n: {
-     o         x: (number) x coordinate of the right anchor
-     o         y: (number) y coordinate of the right anchor
-     o     }
-     o     start: {
-     o         x: (number) x coordinate of the start of the curve
-     o         y: (number) y coordinate of the start of the curve
-     o     }
-     o     end: {
-     o         x: (number) x coordinate of the end of the curve
-     o         y: (number) y coordinate of the end of the curve
-     o     }
-     o     alpha: (number) angle of the curve derivative at the point
-     o }
-    \*/
-    R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
-        var t1 = 1 - t,
-            t13 = pow(t1, 3),
-            t12 = pow(t1, 2),
-            t2 = t * t,
-            t3 = t2 * t,
-            x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x,
-            y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y,
-            mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x),
-            my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y),
-            nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x),
-            ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y),
-            ax = t1 * p1x + t * c1x,
-            ay = t1 * p1y + t * c1y,
-            cx = t1 * c2x + t * p2x,
-            cy = t1 * c2y + t * p2y,
-            alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI);
-        (mx > nx || my < ny) && (alpha += 180);
-        return {
-            x: x,
-            y: y,
-            m: {x: mx, y: my},
-            n: {x: nx, y: ny},
-            start: {x: ax, y: ay},
-            end: {x: cx, y: cy},
-            alpha: alpha
-        };
-    };
-    /*\
-     * Raphael.bezierBBox
-     [ method ]
-     **
-     * Utility method
-     **
-     * Return bounding box of a given cubic bezier curve
-     > Parameters
-     - p1x (number) x of the first point of the curve
-     - p1y (number) y of the first point of the curve
-     - c1x (number) x of the first anchor of the curve
-     - c1y (number) y of the first anchor of the curve
-     - c2x (number) x of the second anchor of the curve
-     - c2y (number) y of the second anchor of the curve
-     - p2x (number) x of the second point of the curve
-     - p2y (number) y of the second point of the curve
-     * or
-     - bez (array) array of six points for bezier curve
-     = (object) point information in format:
-     o {
-     o     min: {
-     o         x: (number) x coordinate of the left point
-     o         y: (number) y coordinate of the top point
-     o     }
-     o     max: {
-     o         x: (number) x coordinate of the right point
-     o         y: (number) y coordinate of the bottom point
-     o     }
-     o }
-    \*/
-    R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
-        if (!R.is(p1x, "array")) {
-            p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y];
-        }
-        var bbox = curveDim.apply(null, p1x);
-        return {
-            x: bbox.min.x,
-            y: bbox.min.y,
-            x2: bbox.max.x,
-            y2: bbox.max.y,
-            width: bbox.max.x - bbox.min.x,
-            height: bbox.max.y - bbox.min.y
-        };
-    };
-    /*\
-     * Raphael.isPointInsideBBox
-     [ method ]
-     **
-     * Utility method
-     **
-     * Returns `true` if given point is inside bounding boxes.
-     > Parameters
-     - bbox (string) bounding box
-     - x (string) x coordinate of the point
-     - y (string) y coordinate of the point
-     = (boolean) `true` if point inside
-    \*/
-    R.isPointInsideBBox = function (bbox, x, y) {
-        return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2;
-    };
-    /*\
-     * Raphael.isBBoxIntersect
-     [ method ]
-     **
-     * Utility method
-     **
-     * Returns `true` if two bounding boxes intersect
-     > Parameters
-     - bbox1 (string) first bounding box
-     - bbox2 (string) second bounding box
-     = (boolean) `true` if they intersect
-    \*/
-    R.isBBoxIntersect = function (bbox1, bbox2) {
-        var i = R.isPointInsideBBox;
-        return i(bbox2, bbox1.x, bbox1.y)
-            || i(bbox2, bbox1.x2, bbox1.y)
-            || i(bbox2, bbox1.x, bbox1.y2)
-            || i(bbox2, bbox1.x2, bbox1.y2)
-            || i(bbox1, bbox2.x, bbox2.y)
-            || i(bbox1, bbox2.x2, bbox2.y)
-            || i(bbox1, bbox2.x, bbox2.y2)
-            || i(bbox1, bbox2.x2, bbox2.y2)
-            || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x)
-            && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y);
-    };
-    function base3(t, p1, p2, p3, p4) {
-        var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
-            t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
-        return t * t2 - 3 * p1 + 3 * p2;
-    }
-    function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) {
-        if (z == null) {
-            z = 1;
-        }
-        z = z > 1 ? 1 : z < 0 ? 0 : z;
-        var z2 = z / 2,
-            n = 12,
-            Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816],
-            Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472],
-            sum = 0;
-        for (var i = 0; i < n; i++) {
-            var ct = z2 * Tvalues[i] + z2,
-                xbase = base3(ct, x1, x2, x3, x4),
-                ybase = base3(ct, y1, y2, y3, y4),
-                comb = xbase * xbase + ybase * ybase;
-            sum += Cvalues[i] * math.sqrt(comb);
-        }
-        return z2 * sum;
-    }
-    function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) {
-        if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) {
-            return;
-        }
-        var t = 1,
-            step = t / 2,
-            t2 = t - step,
-            l,
-            e = .01;
-        l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
-        while (abs(l - ll) > e) {
-            step /= 2;
-            t2 += (l < ll ? 1 : -1) * step;
-            l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
-        }
-        return t2;
-    }
-    function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
-        if (
-            mmax(x1, x2) < mmin(x3, x4) ||
-            mmin(x1, x2) > mmax(x3, x4) ||
-            mmax(y1, y2) < mmin(y3, y4) ||
-            mmin(y1, y2) > mmax(y3, y4)
-        ) {
-            return;
-        }
-        var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
-            ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
-            denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
-
-        if (!denominator) {
-            return;
-        }
-        var px = nx / denominator,
-            py = ny / denominator,
-            px2 = +px.toFixed(2),
-            py2 = +py.toFixed(2);
-        if (
-            px2 < +mmin(x1, x2).toFixed(2) ||
-            px2 > +mmax(x1, x2).toFixed(2) ||
-            px2 < +mmin(x3, x4).toFixed(2) ||
-            px2 > +mmax(x3, x4).toFixed(2) ||
-            py2 < +mmin(y1, y2).toFixed(2) ||
-            py2 > +mmax(y1, y2).toFixed(2) ||
-            py2 < +mmin(y3, y4).toFixed(2) ||
-            py2 > +mmax(y3, y4).toFixed(2)
-        ) {
-            return;
-        }
-        return {x: px, y: py};
-    }
-    function inter(bez1, bez2) {
-        return interHelper(bez1, bez2);
-    }
-    function interCount(bez1, bez2) {
-        return interHelper(bez1, bez2, 1);
-    }
-    function interHelper(bez1, bez2, justCount) {
-        var bbox1 = R.bezierBBox(bez1),
-            bbox2 = R.bezierBBox(bez2);
-        if (!R.isBBoxIntersect(bbox1, bbox2)) {
-            return justCount ? 0 : [];
-        }
-        var l1 = bezlen.apply(0, bez1),
-            l2 = bezlen.apply(0, bez2),
-            n1 = mmax(~~(l1 / 5), 1),
-            n2 = mmax(~~(l2 / 5), 1),
-            dots1 = [],
-            dots2 = [],
-            xy = {},
-            res = justCount ? 0 : [];
-        for (var i = 0; i < n1 + 1; i++) {
-            var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1));
-            dots1.push({x: p.x, y: p.y, t: i / n1});
-        }
-        for (i = 0; i < n2 + 1; i++) {
-            p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2));
-            dots2.push({x: p.x, y: p.y, t: i / n2});
-        }
-        for (i = 0; i < n1; i++) {
-            for (var j = 0; j < n2; j++) {
-                var di = dots1[i],
-                    di1 = dots1[i + 1],
-                    dj = dots2[j],
-                    dj1 = dots2[j + 1],
-                    ci = abs(di1.x - di.x) < .001 ? "y" : "x",
-                    cj = abs(dj1.x - dj.x) < .001 ? "y" : "x",
-                    is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
-                if (is) {
-                    if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) {
-                        continue;
-                    }
-                    xy[is.x.toFixed(4)] = is.y.toFixed(4);
-                    var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t),
-                        t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
-                    if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) {
-                        if (justCount) {
-                            res++;
-                        } else {
-                            res.push({
-                                x: is.x,
-                                y: is.y,
-                                t1: mmin(t1, 1),
-                                t2: mmin(t2, 1)
-                            });
-                        }
-                    }
-                }
-            }
-        }
-        return res;
-    }
-    /*\
-     * Raphael.pathIntersection
-     [ method ]
-     **
-     * Utility method
-     **
-     * Finds intersections of two paths
-     > Parameters
-     - path1 (string) path string
-     - path2 (string) path string
-     = (array) dots of intersection
-     o [
-     o     {
-     o         x: (number) x coordinate of the point
-     o         y: (number) y coordinate of the point
-     o         t1: (number) t value for segment of path1
-     o         t2: (number) t value for segment of path2
-     o         segment1: (number) order number for segment of path1
-     o         segment2: (number) order number for segment of path2
-     o         bez1: (array) eight coordinates representing beziér curve for the segment of path1
-     o         bez2: (array) eight coordinates representing beziér curve for the segment of path2
-     o     }
-     o ]
-    \*/
-    R.pathIntersection = function (path1, path2) {
-        return interPathHelper(path1, path2);
-    };
-    R.pathIntersectionNumber = function (path1, path2) {
-        return interPathHelper(path1, path2, 1);
-    };
-    function interPathHelper(path1, path2, justCount) {
-        path1 = R._path2curve(path1);
-        path2 = R._path2curve(path2);
-        var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2,
-            res = justCount ? 0 : [];
-        for (var i = 0, ii = path1.length; i < ii; i++) {
-            var pi = path1[i];
-            if (pi[0] == "M") {
-                x1 = x1m = pi[1];
-                y1 = y1m = pi[2];
-            } else {
-                if (pi[0] == "C") {
-                    bez1 = [x1, y1].concat(pi.slice(1));
-                    x1 = bez1[6];
-                    y1 = bez1[7];
-                } else {
-                    bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m];
-                    x1 = x1m;
-                    y1 = y1m;
-                }
-                for (var j = 0, jj = path2.length; j < jj; j++) {
-                    var pj = path2[j];
-                    if (pj[0] == "M") {
-                        x2 = x2m = pj[1];
-                        y2 = y2m = pj[2];
-                    } else {
-                        if (pj[0] == "C") {
-                            bez2 = [x2, y2].concat(pj.slice(1));
-                            x2 = bez2[6];
-                            y2 = bez2[7];
-                        } else {
-                            bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m];
-                            x2 = x2m;
-                            y2 = y2m;
-                        }
-                        var intr = interHelper(bez1, bez2, justCount);
-                        if (justCount) {
-                            res += intr;
-                        } else {
-                            for (var k = 0, kk = intr.length; k < kk; k++) {
-                                intr[k].segment1 = i;
-                                intr[k].segment2 = j;
-                                intr[k].bez1 = bez1;
-                                intr[k].bez2 = bez2;
-                            }
-                            res = res.concat(intr);
-                        }
-                    }
-                }
-            }
-        }
-        return res;
-    }
-    /*\
-     * Raphael.isPointInsidePath
-     [ method ]
-     **
-     * Utility method
-     **
-     * Returns `true` if given point is inside a given closed path.
-     > Parameters
-     - path (string) path string
-     - x (number) x of the point
-     - y (number) y of the point
-     = (boolean) true, if point is inside the path
-    \*/
-    R.isPointInsidePath = function (path, x, y) {
-        var bbox = R.pathBBox(path);
-        return R.isPointInsideBBox(bbox, x, y) &&
-               interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1;
-    };
-    R._removedFactory = function (methodname) {
-        return function () {
-            eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname);
-        };
-    };
-    /*\
-     * Raphael.pathBBox
-     [ method ]
-     **
-     * Utility method
-     **
-     * Return bounding box of a given path
-     > Parameters
-     - path (string) path string
-     = (object) bounding box
-     o {
-     o     x: (number) x coordinate of the left top point of the box
-     o     y: (number) y coordinate of the left top point of the box
-     o     x2: (number) x coordinate of the right bottom point of the box
-     o     y2: (number) y coordinate of the right bottom point of the box
-     o     width: (number) width of the box
-     o     height: (number) height of the box
-     o     cx: (number) x coordinate of the center of the box
-     o     cy: (number) y coordinate of the center of the box
-     o }
-    \*/
-    var pathDimensions = R.pathBBox = function (path) {
-        var pth = paths(path);
-        if (pth.bbox) {
-            return clone(pth.bbox);
-        }
-        if (!path) {
-            return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0};
-        }
-        path = path2curve(path);
-        var x = 0,
-            y = 0,
-            X = [],
-            Y = [],
-            p;
-        for (var i = 0, ii = path.length; i < ii; i++) {
-            p = path[i];
-            if (p[0] == "M") {
-                x = p[1];
-                y = p[2];
-                X.push(x);
-                Y.push(y);
-            } else {
-                var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
-                X = X[concat](dim.min.x, dim.max.x);
-                Y = Y[concat](dim.min.y, dim.max.y);
-                x = p[5];
-                y = p[6];
-            }
-        }
-        var xmin = mmin[apply](0, X),
-            ymin = mmin[apply](0, Y),
-            xmax = mmax[apply](0, X),
-            ymax = mmax[apply](0, Y),
-            width = xmax - xmin,
-            height = ymax - ymin,
-                bb = {
-                x: xmin,
-                y: ymin,
-                x2: xmax,
-                y2: ymax,
-                width: width,
-                height: height,
-                cx: xmin + width / 2,
-                cy: ymin + height / 2
-            };
-        pth.bbox = clone(bb);
-        return bb;
-    },
-        pathClone = function (pathArray) {
-            var res = clone(pathArray);
-            res.toString = R._path2string;
-            return res;
-        },
-        pathToRelative = R._pathToRelative = function (pathArray) {
-            var pth = paths(pathArray);
-            if (pth.rel) {
-                return pathClone(pth.rel);
-            }
-            if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
-                pathArray = R.parsePathString(pathArray);
-            }
-            var res = [],
-                x = 0,
-                y = 0,
-                mx = 0,
-                my = 0,
-                start = 0;
-            if (pathArray[0][0] == "M") {
-                x = pathArray[0][1];
-                y = pathArray[0][2];
-                mx = x;
-                my = y;
-                start++;
-                res.push(["M", x, y]);
-            }
-            for (var i = start, ii = pathArray.length; i < ii; i++) {
-                var r = res[i] = [],
-                    pa = pathArray[i];
-                if (pa[0] != lowerCase.call(pa[0])) {
-                    r[0] = lowerCase.call(pa[0]);
-                    switch (r[0]) {
-                        case "a":
-                            r[1] = pa[1];
-                            r[2] = pa[2];
-                            r[3] = pa[3];
-                            r[4] = pa[4];
-                            r[5] = pa[5];
-                            r[6] = +(pa[6] - x).toFixed(3);
-                            r[7] = +(pa[7] - y).toFixed(3);
-                            break;
-                        case "v":
-                            r[1] = +(pa[1] - y).toFixed(3);
-                            break;
-                        case "m":
-                            mx = pa[1];
-                            my = pa[2];
-                        default:
-                            for (var j = 1, jj = pa.length; j < jj; j++) {
-                                r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
-                            }
-                    }
-                } else {
-                    r = res[i] = [];
-                    if (pa[0] == "m") {
-                        mx = pa[1] + x;
-                        my = pa[2] + y;
-                    }
-                    for (var k = 0, kk = pa.length; k < kk; k++) {
-                        res[i][k] = pa[k];
-                    }
-                }
-                var len = res[i].length;
-                switch (res[i][0]) {
-                    case "z":
-                        x = mx;
-                        y = my;
-                        break;
-                    case "h":
-                        x += +res[i][len - 1];
-                        break;
-                    case "v":
-                        y += +res[i][len - 1];
-                        break;
-                    default:
-                        x += +res[i][len - 2];
-                        y += +res[i][len - 1];
-                }
-            }
-            res.toString = R._path2string;
-            pth.rel = pathClone(res);
-            return res;
-        },
-        pathToAbsolute = R._pathToAbsolute = function (pathArray) {
-            var pth = paths(pathArray);
-            if (pth.abs) {
-                return pathClone(pth.abs);
-            }
-            if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
-                pathArray = R.parsePathString(pathArray);
-            }
-            if (!pathArray || !pathArray.length) {
-                return [["M", 0, 0]];
-            }
-            var res = [],
-                x = 0,
-                y = 0,
-                mx = 0,
-                my = 0,
-                start = 0;
-            if (pathArray[0][0] == "M") {
-                x = +pathArray[0][1];
-                y = +pathArray[0][2];
-                mx = x;
-                my = y;
-                start++;
-                res[0] = ["M", x, y];
-            }
-            var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z";
-            for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) {
-                res.push(r = []);
-                pa = pathArray[i];
-                if (pa[0] != upperCase.call(pa[0])) {
-                    r[0] = upperCase.call(pa[0]);
-                    switch (r[0]) {
-                        case "A":
-                            r[1] = pa[1];
-                            r[2] = pa[2];
-                            r[3] = pa[3];
-                            r[4] = pa[4];
-                            r[5] = pa[5];
-                            r[6] = +(pa[6] + x);
-                            r[7] = +(pa[7] + y);
-                            break;
-                        case "V":
-                            r[1] = +pa[1] + y;
-                            break;
-                        case "H":
-                            r[1] = +pa[1] + x;
-                            break;
-                        case "R":
-                            var dots = [x, y][concat](pa.slice(1));
-                            for (var j = 2, jj = dots.length; j < jj; j++) {
-                                dots[j] = +dots[j] + x;
-                                dots[++j] = +dots[j] + y;
-                            }
-                            res.pop();
-                            res = res[concat](catmullRom2bezier(dots, crz));
-                            break;
-                        case "M":
-                            mx = +pa[1] + x;
-                            my = +pa[2] + y;
-                        default:
-                            for (j = 1, jj = pa.length; j < jj; j++) {
-                                r[j] = +pa[j] + ((j % 2) ? x : y);
-                            }
-                    }
-                } else if (pa[0] == "R") {
-                    dots = [x, y][concat](pa.slice(1));
-                    res.pop();
-                    res = res[concat](catmullRom2bezier(dots, crz));
-                    r = ["R"][concat](pa.slice(-2));
-                } else {
-                    for (var k = 0, kk = pa.length; k < kk; k++) {
-                        r[k] = pa[k];
-                    }
-                }
-                switch (r[0]) {
-                    case "Z":
-                        x = mx;
-                        y = my;
-                        break;
-                    case "H":
-                        x = r[1];
-                        break;
-                    case "V":
-                        y = r[1];
-                        break;
-                    case "M":
-                        mx = r[r.length - 2];
-                        my = r[r.length - 1];
-                    default:
-                        x = r[r.length - 2];
-                        y = r[r.length - 1];
-                }
-            }
-            res.toString = R._path2string;
-            pth.abs = pathClone(res);
-            return res;
-        },
-        l2c = function (x1, y1, x2, y2) {
-            return [x1, y1, x2, y2, x2, y2];
-        },
-        q2c = function (x1, y1, ax, ay, x2, y2) {
-            var _13 = 1 / 3,
-                _23 = 2 / 3;
-            return [
-                    _13 * x1 + _23 * ax,
-                    _13 * y1 + _23 * ay,
-                    _13 * x2 + _23 * ax,
-                    _13 * y2 + _23 * ay,
-                    x2,
-                    y2
-                ];
-        },
-        a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
-            // for more information of where this math came from visit:
-            // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
-            var _120 = PI * 120 / 180,
-                rad = PI / 180 * (+angle || 0),
-                res = [],
-                xy,
-                rotate = cacher(function (x, y, rad) {
-                    var X = x * math.cos(rad) - y * math.sin(rad),
-                        Y = x * math.sin(rad) + y * math.cos(rad);
-                    return {x: X, y: Y};
-                });
-            if (!recursive) {
-                xy = rotate(x1, y1, -rad);
-                x1 = xy.x;
-                y1 = xy.y;
-                xy = rotate(x2, y2, -rad);
-                x2 = xy.x;
-                y2 = xy.y;
-                var cos = math.cos(PI / 180 * angle),
-                    sin = math.sin(PI / 180 * angle),
-                    x = (x1 - x2) / 2,
-                    y = (y1 - y2) / 2;
-                var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
-                if (h > 1) {
-                    h = math.sqrt(h);
-                    rx = h * rx;
-                    ry = h * ry;
-                }
-                var rx2 = rx * rx,
-                    ry2 = ry * ry,
-                    k = (large_arc_flag == sweep_flag ? -1 : 1) *
-                        math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
-                    cx = k * rx * y / ry + (x1 + x2) / 2,
-                    cy = k * -ry * x / rx + (y1 + y2) / 2,
-                    f1 = math.asin(((y1 - cy) / ry).toFixed(9)),
-                    f2 = math.asin(((y2 - cy) / ry).toFixed(9));
-
-                f1 = x1 < cx ? PI - f1 : f1;
-                f2 = x2 < cx ? PI - f2 : f2;
-                f1 < 0 && (f1 = PI * 2 + f1);
-                f2 < 0 && (f2 = PI * 2 + f2);
-                if (sweep_flag && f1 > f2) {
-                    f1 = f1 - PI * 2;
-                }
-                if (!sweep_flag && f2 > f1) {
-                    f2 = f2 - PI * 2;
-                }
-            } else {
-                f1 = recursive[0];
-                f2 = recursive[1];
-                cx = recursive[2];
-                cy = recursive[3];
-            }
-            var df = f2 - f1;
-            if (abs(df) > _120) {
-                var f2old = f2,
-                    x2old = x2,
-                    y2old = y2;
-                f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
-                x2 = cx + rx * math.cos(f2);
-                y2 = cy + ry * math.sin(f2);
-                res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
-            }
-            df = f2 - f1;
-            var c1 = math.cos(f1),
-                s1 = math.sin(f1),
-                c2 = math.cos(f2),
-                s2 = math.sin(f2),
-                t = math.tan(df / 4),
-                hx = 4 / 3 * rx * t,
-                hy = 4 / 3 * ry * t,
-                m1 = [x1, y1],
-                m2 = [x1 + hx * s1, y1 - hy * c1],
-                m3 = [x2 + hx * s2, y2 - hy * c2],
-                m4 = [x2, y2];
-            m2[0] = 2 * m1[0] - m2[0];
-            m2[1] = 2 * m1[1] - m2[1];
-            if (recursive) {
-                return [m2, m3, m4][concat](res);
-            } else {
-                res = [m2, m3, m4][concat](res).join()[split](",");
-                var newres = [];
-                for (var i = 0, ii = res.length; i < ii; i++) {
-                    newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
-                }
-                return newres;
-            }
-        },
-        findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
-            var t1 = 1 - t;
-            return {
-                x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
-                y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y
-            };
-        },
-        curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
-            var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
-                b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
-                c = p1x - c1x,
-                t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a,
-                t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a,
-                y = [p1y, p2y],
-                x = [p1x, p2x],
-                dot;
-            abs(t1) > "1e12" && (t1 = .5);
-            abs(t2) > "1e12" && (t2 = .5);
-            if (t1 > 0 && t1 < 1) {
-                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
-                x.push(dot.x);
-                y.push(dot.y);
-            }
-            if (t2 > 0 && t2 < 1) {
-                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
-                x.push(dot.x);
-                y.push(dot.y);
-            }
-            a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
-            b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
-            c = p1y - c1y;
-            t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a;
-            t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a;
-            abs(t1) > "1e12" && (t1 = .5);
-            abs(t2) > "1e12" && (t2 = .5);
-            if (t1 > 0 && t1 < 1) {
-                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
-                x.push(dot.x);
-                y.push(dot.y);
-            }
-            if (t2 > 0 && t2 < 1) {
-                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
-                x.push(dot.x);
-                y.push(dot.y);
-            }
-            return {
-                min: {x: mmin[apply](0, x), y: mmin[apply](0, y)},
-                max: {x: mmax[apply](0, x), y: mmax[apply](0, y)}
-            };
-        }),
-        path2curve = R._path2curve = cacher(function (path, path2) {
-            var pth = !path2 && paths(path);
-            if (!path2 && pth.curve) {
-                return pathClone(pth.curve);
-            }
-            var p = pathToAbsolute(path),
-                p2 = path2 && pathToAbsolute(path2),
-                attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
-                attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
-                processPath = function (path, d, pcom) {
-                    var nx, ny, tq = {T:1, Q:1};
-                    if (!path) {
-                        return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
-                    }
-                    !(path[0] in tq) && (d.qx = d.qy = null);
-                    switch (path[0]) {
-                        case "M":
-                            d.X = path[1];
-                            d.Y = path[2];
-                            break;
-                        case "A":
-                            path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1))));
-                            break;
-                        case "S":
-                            if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S.
-                                nx = d.x * 2 - d.bx;          // And reflect the previous
-                                ny = d.y * 2 - d.by;          // command's control point relative to the current point.
-                            }
-                            else {                            // or some else or nothing
-                                nx = d.x;
-                                ny = d.y;
-                            }
-                            path = ["C", nx, ny][concat](path.slice(1));
-                            break;
-                        case "T":
-                            if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T.
-                                d.qx = d.x * 2 - d.qx;        // And make a reflection similar
-                                d.qy = d.y * 2 - d.qy;        // to case "S".
-                            }
-                            else {                            // or something else or nothing
-                                d.qx = d.x;
-                                d.qy = d.y;
-                            }
-                            path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
-                            break;
-                        case "Q":
-                            d.qx = path[1];
-                            d.qy = path[2];
-                            path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
-                            break;
-                        case "L":
-                            path = ["C"][concat](l2c(d.x, d.y, path[1], path[2]));
-                            break;
-                        case "H":
-                            path = ["C"][concat](l2c(d.x, d.y, path[1], d.y));
-                            break;
-                        case "V":
-                            path = ["C"][concat](l2c(d.x, d.y, d.x, path[1]));
-                            break;
-                        case "Z":
-                            path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y));
-                            break;
-                    }
-                    return path;
-                },
-                fixArc = function (pp, i) {
-                    if (pp[i].length > 7) {
-                        pp[i].shift();
-                        var pi = pp[i];
-                        while (pi.length) {
-                            pcoms1[i]="A"; // if created multiple C:s, their original seg is saved
-                            p2 && (pcoms2[i]="A"); // the same as above
-                            pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6)));
-                        }
-                        pp.splice(i, 1);
-                        ii = mmax(p.length, p2 && p2.length || 0);
-                    }
-                },
-                fixM = function (path1, path2, a1, a2, i) {
-                    if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
-                        path2.splice(i, 0, ["M", a2.x, a2.y]);
-                        a1.bx = 0;
-                        a1.by = 0;
-                        a1.x = path1[i][1];
-                        a1.y = path1[i][2];
-                        ii = mmax(p.length, p2 && p2.length || 0);
-                    }
-                },
-                pcoms1 = [], // path commands of original path p
-                pcoms2 = [], // path commands of original path p2
-                pfirst = "", // temporary holder for original path command
-                pcom = ""; // holder for previous path command of original path
-            for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) {
-                p[i] && (pfirst = p[i][0]); // save current path command
-
-                if (pfirst != "C") // C is not saved yet, because it may be result of conversion
-                {
-                    pcoms1[i] = pfirst; // Save current path command
-                    i && ( pcom = pcoms1[i-1]); // Get previous path command pcom
-                }
-                p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath
-
-                if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command
-                // which may produce multiple C:s
-                // so we have to make sure that C is also C in original path
-
-                fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1
-
-                if (p2) { // the same procedures is done to p2
-                    p2[i] && (pfirst = p2[i][0]);
-                    if (pfirst != "C")
-                    {
-                        pcoms2[i] = pfirst;
-                        i && (pcom = pcoms2[i-1]);
-                    }
-                    p2[i] = processPath(p2[i], attrs2, pcom);
-
-                    if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C";
-
-                    fixArc(p2, i);
-                }
-                fixM(p, p2, attrs, attrs2, i);
-                fixM(p2, p, attrs2, attrs, i);
-                var seg = p[i],
-                    seg2 = p2 && p2[i],
-                    seglen = seg.length,
-                    seg2len = p2 && seg2.length;
-                attrs.x = seg[seglen - 2];
-                attrs.y = seg[seglen - 1];
-                attrs.bx = toFloat(seg[seglen - 4]) || attrs.x;
-                attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
-                attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x);
-                attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y);
-                attrs2.x = p2 && seg2[seg2len - 2];
-                attrs2.y = p2 && seg2[seg2len - 1];
-            }
-            if (!p2) {
-                pth.curve = pathClone(p);
-            }
-            return p2 ? [p, p2] : p;
-        }, null, pathClone),
-        parseDots = R._parseDots = cacher(function (gradient) {
-            var dots = [];
-            for (var i = 0, ii = gradient.length; i < ii; i++) {
-                var dot = {},
-                    par = gradient[i].match(/^([^:]*):?([\d\.]*)/);
-                dot.color = R.getRGB(par[1]);
-                if (dot.color.error) {
-                    return null;
-                }
-                dot.color = dot.color.hex;
-                par[2] && (dot.offset = par[2] + "%");
-                dots.push(dot);
-            }
-            for (i = 1, ii = dots.length - 1; i < ii; i++) {
-                if (!dots[i].offset) {
-                    var start = toFloat(dots[i - 1].offset || 0),
-                        end = 0;
-                    for (var j = i + 1; j < ii; j++) {
-                        if (dots[j].offset) {
-                            end = dots[j].offset;
-                            break;
-                        }
-                    }
-                    if (!end) {
-                        end = 100;
-                        j = ii;
-                    }
-                    end = toFloat(end);
-                    var d = (end - start) / (j - i + 1);
-                    for (; i < j; i++) {
-                        start += d;
-                        dots[i].offset = start + "%";
-                    }
-                }
-            }
-            return dots;
-        }),
-        tear = R._tear = function (el, paper) {
-            el == paper.top && (paper.top = el.prev);
-            el == paper.bottom && (paper.bottom = el.next);
-            el.next && (el.next.prev = el.prev);
-            el.prev && (el.prev.next = el.next);
-        },
-        tofront = R._tofront = function (el, paper) {
-            if (paper.top === el) {
-                return;
-            }
-            tear(el, paper);
-            el.next = null;
-            el.prev = paper.top;
-            paper.top.next = el;
-            paper.top = el;
-        },
-        toback = R._toback = function (el, paper) {
-            if (paper.bottom === el) {
-                return;
-            }
-            tear(el, paper);
-            el.next = paper.bottom;
-            el.prev = null;
-            paper.bottom.prev = el;
-            paper.bottom = el;
-        },
-        insertafter = R._insertafter = function (el, el2, paper) {
-            tear(el, paper);
-            el2 == paper.top && (paper.top = el);
-            el2.next && (el2.next.prev = el);
-            el.next = el2.next;
-            el.prev = el2;
-            el2.next = el;
-        },
-        insertbefore = R._insertbefore = function (el, el2, paper) {
-            tear(el, paper);
-            el2 == paper.bottom && (paper.bottom = el);
-            el2.prev && (el2.prev.next = el);
-            el.prev = el2.prev;
-            el2.prev = el;
-            el.next = el2;
-        },
-        /*\
-         * Raphael.toMatrix
-         [ method ]
-         **
-         * Utility method
-         **
-         * Returns matrix of transformations applied to a given path
-         > Parameters
-         - path (string) path string
-         - transform (string|array) transformation string
-         = (object) @Matrix
-        \*/
-        toMatrix = R.toMatrix = function (path, transform) {
-            var bb = pathDimensions(path),
-                el = {
-                    _: {
-                        transform: E
-                    },
-                    getBBox: function () {
-                        return bb;
-                    }
-                };
-            extractTransform(el, transform);
-            return el.matrix;
-        },
-        /*\
-         * Raphael.transformPath
-         [ method ]
-         **
-         * Utility method
-         **
-         * Returns path transformed by a given transformation
-         > Parameters
-         - path (string) path string
-         - transform (string|array) transformation string
-         = (string) path
-        \*/
-        transformPath = R.transformPath = function (path, transform) {
-            return mapPath(path, toMatrix(path, transform));
-        },
-        extractTransform = R._extractTransform = function (el, tstr) {
-            if (tstr == null) {
-                return el._.transform;
-            }
-            tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E);
-            var tdata = R.parseTransformString(tstr),
-                deg = 0,
-                dx = 0,
-                dy = 0,
-                sx = 1,
-                sy = 1,
-                _ = el._,
-                m = new Matrix;
-            _.transform = tdata || [];
-            if (tdata) {
-                for (var i = 0, ii = tdata.length; i < ii; i++) {
-                    var t = tdata[i],
-                        tlen = t.length,
-                        command = Str(t[0]).toLowerCase(),
-                        absolute = t[0] != command,
-                        inver = absolute ? m.invert() : 0,
-                        x1,
-                        y1,
-                        x2,
-                        y2,
-                        bb;
-                    if (command == "t" && tlen == 3) {
-                        if (absolute) {
-                            x1 = inver.x(0, 0);
-                            y1 = inver.y(0, 0);
-                            x2 = inver.x(t[1], t[2]);
-                            y2 = inver.y(t[1], t[2]);
-                            m.translate(x2 - x1, y2 - y1);
-                        } else {
-                            m.translate(t[1], t[2]);
-                        }
-                    } else if (command == "r") {
-                        if (tlen == 2) {
-                            bb = bb || el.getBBox(1);
-                            m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2);
-                            deg += t[1];
-                        } else if (tlen == 4) {
-                            if (absolute) {
-                                x2 = inver.x(t[2], t[3]);
-                                y2 = inver.y(t[2], t[3]);
-                                m.rotate(t[1], x2, y2);
-                            } else {
-                                m.rotate(t[1], t[2], t[3]);
-                            }
-                            deg += t[1];
-                        }
-                    } else if (command == "s") {
-                        if (tlen == 2 || tlen == 3) {
-                            bb = bb || el.getBBox(1);
-                            m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2);
-                            sx *= t[1];
-                            sy *= t[tlen - 1];
-                        } else if (tlen == 5) {
-                            if (absolute) {
-                                x2 = inver.x(t[3], t[4]);
-                                y2 = inver.y(t[3], t[4]);
-                                m.scale(t[1], t[2], x2, y2);
-                            } else {
-                                m.scale(t[1], t[2], t[3], t[4]);
-                            }
-                            sx *= t[1];
-                            sy *= t[2];
-                        }
-                    } else if (command == "m" && tlen == 7) {
-                        m.add(t[1], t[2], t[3], t[4], t[5], t[6]);
-                    }
-                    _.dirtyT = 1;
-                    el.matrix = m;
-                }
-            }
-
-            /*\
-             * Element.matrix
-             [ property (object) ]
-             **
-             * Keeps @Matrix object, which represents element transformation
-            \*/
-            el.matrix = m;
-
-            _.sx = sx;
-            _.sy = sy;
-            _.deg = deg;
-            _.dx = dx = m.e;
-            _.dy = dy = m.f;
-
-            if (sx == 1 && sy == 1 && !deg && _.bbox) {
-                _.bbox.x += +dx;
-                _.bbox.y += +dy;
-            } else {
-                _.dirtyT = 1;
-            }
-        },
-        getEmpty = function (item) {
-            var l = item[0];
-            switch (l.toLowerCase()) {
-                case "t": return [l, 0, 0];
-                case "m": return [l, 1, 0, 0, 1, 0, 0];
-                case "r": if (item.length == 4) {
-                    return [l, 0, item[2], item[3]];
-                } else {
-                    return [l, 0];
-                }
-                case "s": if (item.length == 5) {
-                    return [l, 1, 1, item[3], item[4]];
-                } else if (item.length == 3) {
-                    return [l, 1, 1];
-                } else {
-                    return [l, 1];
-                }
-            }
-        },
-        equaliseTransform = R._equaliseTransform = function (t1, t2) {
-            t2 = Str(t2).replace(/\.{3}|\u2026/g, t1);
-            t1 = R.parseTransformString(t1) || [];
-            t2 = R.parseTransformString(t2) || [];
-            var maxlength = mmax(t1.length, t2.length),
-                from = [],
-                to = [],
-                i = 0, j, jj,
-                tt1, tt2;
-            for (; i < maxlength; i++) {
-                tt1 = t1[i] || getEmpty(t2[i]);
-                tt2 = t2[i] || getEmpty(tt1);
-                if ((tt1[0] != tt2[0]) ||
-                    (tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) ||
-                    (tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4]))
-                    ) {
-                    return;
-                }
-                from[i] = [];
-                to[i] = [];
-                for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) {
-                    j in tt1 && (from[i][j] = tt1[j]);
-                    j in tt2 && (to[i][j] = tt2[j]);
-                }
-            }
-            return {
-                from: from,
-                to: to
-            };
-        };
-    R._getContainer = function (x, y, w, h) {
-        var container;
-        container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x;
-        if (container == null) {
-            return;
-        }
-        if (container.tagName) {
-            if (y == null) {
-                return {
-                    container: container,
-                    width: container.style.pixelWidth || container.offsetWidth,
-                    height: container.style.pixelHeight || container.offsetHeight
-                };
-            } else {
-                return {
-                    container: container,
-                    width: y,
-                    height: w
-                };
-            }
-        }
-        return {
-            container: 1,
-            x: x,
-            y: y,
-            width: w,
-            height: h
-        };
-    };
-    /*\
-     * Raphael.pathToRelative
-     [ method ]
-     **
-     * Utility method
-     **
-     * Converts path to relative form
-     > Parameters
-     - pathString (string|array) path string or array of segments
-     = (array) array of segments.
-    \*/
-    R.pathToRelative = pathToRelative;
-    R._engine = {};
-    /*\
-     * Raphael.path2curve
-     [ method ]
-     **
-     * Utility method
-     **
-     * Converts path to a new path where all segments are cubic bezier curves.
-     > Parameters
-     - pathString (string|array) path string or array of segments
-     = (array) array of segments.
-    \*/
-    R.path2curve = path2curve;
-    /*\
-     * Raphael.matrix
-     [ method ]
-     **
-     * Utility method
-     **
-     * Returns matrix based on given parameters.
-     > Parameters
-     - a (number)
-     - b (number)
-     - c (number)
-     - d (number)
-     - e (number)
-     - f (number)
-     = (object) @Matrix
-    \*/
-    R.matrix = function (a, b, c, d, e, f) {
-        return new Matrix(a, b, c, d, e, f);
-    };
-    function Matrix(a, b, c, d, e, f) {
-        if (a != null) {
-            this.a = +a;
-            this.b = +b;
-            this.c = +c;
-            this.d = +d;
-            this.e = +e;
-            this.f = +f;
-        } else {
-            this.a = 1;
-            this.b = 0;
-            this.c = 0;
-            this.d = 1;
-            this.e = 0;
-            this.f = 0;
-        }
-    }
-    (function (matrixproto) {
-        /*\
-         * Matrix.add
-         [ method ]
-         **
-         * Adds given matrix to existing one.
-         > Parameters
-         - a (number)
-         - b (number)
-         - c (number)
-         - d (number)
-         - e (number)
-         - f (number)
-         or
-         - matrix (object) @Matrix
-        \*/
-        matrixproto.add = function (a, b, c, d, e, f) {
-            var out = [[], [], []],
-                m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]],
-                matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
-                x, y, z, res;
-
-            if (a && a instanceof Matrix) {
-                matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]];
-            }
-
-            for (x = 0; x < 3; x++) {
-                for (y = 0; y < 3; y++) {
-                    res = 0;
-                    for (z = 0; z < 3; z++) {
-                        res += m[x][z] * matrix[z][y];
-                    }
-                    out[x][y] = res;
-                }
-            }
-            this.a = out[0][0];
-            this.b = out[1][0];
-            this.c = out[0][1];
-            this.d = out[1][1];
-            this.e = out[0][2];
-            this.f = out[1][2];
-        };
-        /*\
-         * Matrix.invert
-         [ method ]
-         **
-         * Returns inverted version of the matrix
-         = (object) @Matrix
-        \*/
-        matrixproto.invert = function () {
-            var me = this,
-                x = me.a * me.d - me.b * me.c;
-            return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x);
-        };
-        /*\
-         * Matrix.clone
-         [ method ]
-         **
-         * Returns copy of the matrix
-         = (object) @Matrix
-        \*/
-        matrixproto.clone = function () {
-            return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f);
-        };
-        /*\
-         * Matrix.translate
-         [ method ]
-         **
-         * Translate the matrix
-         > Parameters
-         - x (number)
-         - y (number)
-        \*/
-        matrixproto.translate = function (x, y) {
-            this.add(1, 0, 0, 1, x, y);
-        };
-        /*\
-         * Matrix.scale
-         [ method ]
-         **
-         * Scales the matrix
-         > Parameters
-         - x (number)
-         - y (number) #optional
-         - cx (number) #optional
-         - cy (number) #optional
-        \*/
-        matrixproto.scale = function (x, y, cx, cy) {
-            y == null && (y = x);
-            (cx || cy) && this.add(1, 0, 0, 1, cx, cy);
-            this.add(x, 0, 0, y, 0, 0);
-            (cx || cy) && this.add(1, 0, 0, 1, -cx, -cy);
-        };
-        /*\
-         * Matrix.rotate
-         [ method ]
-         **
-         * Rotates the matrix
-         > Parameters
-         - a (number)
-         - x (number)
-         - y (number)
-        \*/
-        matrixproto.rotate = function (a, x, y) {
-            a = R.rad(a);
-            x = x || 0;
-            y = y || 0;
-            var cos = +math.cos(a).toFixed(9),
-                sin = +math.sin(a).toFixed(9);
-            this.add(cos, sin, -sin, cos, x, y);
-            this.add(1, 0, 0, 1, -x, -y);
-        };
-        /*\
-         * Matrix.x
-         [ method ]
-         **
-         * Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y
-         > Parameters
-         - x (number)
-         - y (number)
-         = (number) x
-        \*/
-        matrixproto.x = function (x, y) {
-            return x * this.a + y * this.c + this.e;
-        };
-        /*\
-         * Matrix.y
-         [ method ]
-         **
-         * Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x
-         > Parameters
-         - x (number)
-         - y (number)
-         = (number) y
-        \*/
-        matrixproto.y = function (x, y) {
-            return x * this.b + y * this.d + this.f;
-        };
-        matrixproto.get = function (i) {
-            return +this[Str.fromCharCode(97 + i)].toFixed(4);
-        };
-        matrixproto.toString = function () {
-            return R.svg ?
-                "matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" :
-                [this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join();
-        };
-        matrixproto.toFilter = function () {
-            return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) +
-                ", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) +
-                ", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')";
-        };
-        matrixproto.offset = function () {
-            return [this.e.toFixed(4), this.f.toFixed(4)];
-        };
-        function norm(a) {
-            return a[0] * a[0] + a[1] * a[1];
-        }
-        function normalize(a) {
-            var mag = math.sqrt(norm(a));
-            a[0] && (a[0] /= mag);
-            a[1] && (a[1] /= mag);
-        }
-        /*\
-         * Matrix.split
-         [ method ]
-         **
-         * Splits matrix into primitive transformations
-         = (object) in format:
-         o dx (number) translation by x
-         o dy (number) translation by y
-         o scalex (number) scale by x
-         o scaley (number) scale by y
-         o shear (number) shear
-         o rotate (number) rotation in deg
-         o isSimple (boolean) could it be represented via simple transformations
-        \*/
-        matrixproto.split = function () {
-            var out = {};
-            // translation
-            out.dx = this.e;
-            out.dy = this.f;
-
-            // scale and shear
-            var row = [[this.a, this.c], [this.b, this.d]];
-            out.scalex = math.sqrt(norm(row[0]));
-            normalize(row[0]);
-
-            out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
-            row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
-
-            out.scaley = math.sqrt(norm(row[1]));
-            normalize(row[1]);
-            out.shear /= out.scaley;
-
-            // rotation
-            var sin = -row[0][1],
-                cos = row[1][1];
-            if (cos < 0) {
-                out.rotate = R.deg(math.acos(cos));
-                if (sin < 0) {
-                    out.rotate = 360 - out.rotate;
-                }
-            } else {
-                out.rotate = R.deg(math.asin(sin));
-            }
-
-            out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate);
-            out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate;
-            out.noRotation = !+out.shear.toFixed(9) && !out.rotate;
-            return out;
-        };
-        /*\
-         * Matrix.toTransformString
-         [ method ]
-         **
-         * Return transform string that represents given matrix
-         = (string) transform string
-        \*/
-        matrixproto.toTransformString = function (shorter) {
-            var s = shorter || this[split]();
-            if (s.isSimple) {
-                s.scalex = +s.scalex.toFixed(4);
-                s.scaley = +s.scaley.toFixed(4);
-                s.rotate = +s.rotate.toFixed(4);
-                return  (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) +
-                        (s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) +
-                        (s.rotate ? "r" + [s.rotate, 0, 0] : E);
-            } else {
-                return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)];
-            }
-        };
-    })(Matrix.prototype);
-
-    // WebKit rendering bug workaround method
-    var version = navigator.userAgent.match(/Version\/(.*?)\s/) || navigator.userAgent.match(/Chrome\/(\d+)/);
-    if ((navigator.vendor == "Apple Computer, Inc.") && (version && version[1] < 4 || navigator.platform.slice(0, 2) == "iP") ||
-        (navigator.vendor == "Google Inc." && version && version[1] < 8)) {
-        /*\
-         * Paper.safari
-         [ method ]
-         **
-         * There is an inconvenient rendering bug in Safari (WebKit):
-         * sometimes the rendering should be forced.
-         * This method should help with dealing with this bug.
-        \*/
-        paperproto.safari = function () {
-            var rect = this.rect(-99, -99, this.width + 99, this.height + 99).attr({stroke: "none"});
-            setTimeout(function () {rect.remove();});
-        };
-    } else {
-        paperproto.safari = fun;
-    }
-
-    var preventDefault = function () {
-        this.returnValue = false;
-    },
-    preventTouch = function () {
-        return this.originalEvent.preventDefault();
-    },
-    stopPropagation = function () {
-        this.cancelBubble = true;
-    },
-    stopTouch = function () {
-        return this.originalEvent.stopPropagation();
-    },
-    getEventPosition = function (e) {
-        var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
-            scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
-
-        return {
-            x: e.clientX + scrollX,
-            y: e.clientY + scrollY
-        };
-    },
-    addEvent = (function () {
-        if (g.doc.addEventListener) {
-            return function (obj, type, fn, element) {
-                var f = function (e) {
-                    var pos = getEventPosition(e);
-                    return fn.call(element, e, pos.x, pos.y);
-                };
-                obj.addEventListener(type, f, false);
-
-                if (supportsTouch && touchMap[type]) {
-                    var _f = function (e) {
-                        var pos = getEventPosition(e),
-                            olde = e;
-
-                        for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) {
-                            if (e.targetTouches[i].target == obj) {
-                                e = e.targetTouches[i];
-                                e.originalEvent = olde;
-                                e.preventDefault = preventTouch;
-                                e.stopPropagation = stopTouch;
-                                break;
-                            }
-                        }
-
-                        return fn.call(element, e, pos.x, pos.y);
-                    };
-                    obj.addEventListener(touchMap[type], _f, false);
-                }
-
-                return function () {
-                    obj.removeEventListener(type, f, false);
-
-                    if (supportsTouch && touchMap[type])
-                        obj.removeEventListener(touchMap[type], _f, false);
-
-                    return true;
-                };
-            };
-        } else if (g.doc.attachEvent) {
-            return function (obj, type, fn, element) {
-                var f = function (e) {
-                    e = e || g.win.event;
-                    var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
-                        scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
-                        x = e.clientX + scrollX,
-                        y = e.clientY + scrollY;
-                    e.preventDefault = e.preventDefault || preventDefault;
-                    e.stopPropagation = e.stopPropagation || stopPropagation;
-                    return fn.call(element, e, x, y);
-                };
-                obj.attachEvent("on" + type, f);
-                var detacher = function () {
-                    obj.detachEvent("on" + type, f);
-                    return true;
-                };
-                return detacher;
-            };
-        }
-    })(),
-    drag = [],
-    dragMove = function (e) {
-        var x = e.clientX,
-            y = e.clientY,
-            scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
-            scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
-            dragi,
-            j = drag.length;
-        while (j--) {
-            dragi = drag[j];
-            if (supportsTouch && e.touches) {
-                var i = e.touches.length,
-                    touch;
-                while (i--) {
-                    touch = e.touches[i];
-                    if (touch.identifier == dragi.el._drag.id) {
-                        x = touch.clientX;
-                        y = touch.clientY;
-                        (e.originalEvent ? e.originalEvent : e).preventDefault();
-                        break;
-                    }
-                }
-            } else {
-                e.preventDefault();
-            }
-            var node = dragi.el.node,
-                o,
-                next = node.nextSibling,
-                parent = node.parentNode,
-                display = node.style.display;
-            g.win.opera && parent.removeChild(node);
-            node.style.display = "none";
-            o = dragi.el.paper.getElementByPoint(x, y);
-            node.style.display = display;
-            g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node));
-            o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o);
-            x += scrollX;
-            y += scrollY;
-            eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e);
-        }
-    },
-    dragUp = function (e) {
-        R.unmousemove(dragMove).unmouseup(dragUp);
-        var i = drag.length,
-            dragi;
-        while (i--) {
-            dragi = drag[i];
-            dragi.el._drag = {};
-            eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e);
-        }
-        drag = [];
-    },
-    /*\
-     * Raphael.el
-     [ property (object) ]
-     **
-     * You can add your own method to elements. This is usefull when you want to hack default functionality or
-     * want to wrap some common transformation or attributes in one method. In difference to canvas methods,
-     * you can redefine element method at any time. Expending element methods wouldn’t affect set.
-     > Usage
-     | Raphael.el.red = function () {
-     |     this.attr({fill: "#f00"});
-     | };
-     | // then use it
-     | paper.circle(100, 100, 20).red();
-    \*/
-    elproto = R.el = {};
-    /*\
-     * Element.click
-     [ method ]
-     **
-     * Adds event handler for click for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unclick
-     [ method ]
-     **
-     * Removes event handler for click for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.dblclick
-     [ method ]
-     **
-     * Adds event handler for double click for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.undblclick
-     [ method ]
-     **
-     * Removes event handler for double click for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.mousedown
-     [ method ]
-     **
-     * Adds event handler for mousedown for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unmousedown
-     [ method ]
-     **
-     * Removes event handler for mousedown for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.mousemove
-     [ method ]
-     **
-     * Adds event handler for mousemove for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unmousemove
-     [ method ]
-     **
-     * Removes event handler for mousemove for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.mouseout
-     [ method ]
-     **
-     * Adds event handler for mouseout for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unmouseout
-     [ method ]
-     **
-     * Removes event handler for mouseout for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.mouseover
-     [ method ]
-     **
-     * Adds event handler for mouseover for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unmouseover
-     [ method ]
-     **
-     * Removes event handler for mouseover for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.mouseup
-     [ method ]
-     **
-     * Adds event handler for mouseup for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.unmouseup
-     [ method ]
-     **
-     * Removes event handler for mouseup for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.touchstart
-     [ method ]
-     **
-     * Adds event handler for touchstart for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.untouchstart
-     [ method ]
-     **
-     * Removes event handler for touchstart for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.touchmove
-     [ method ]
-     **
-     * Adds event handler for touchmove for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.untouchmove
-     [ method ]
-     **
-     * Removes event handler for touchmove for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.touchend
-     [ method ]
-     **
-     * Adds event handler for touchend for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.untouchend
-     [ method ]
-     **
-     * Removes event handler for touchend for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-
-    /*\
-     * Element.touchcancel
-     [ method ]
-     **
-     * Adds event handler for touchcancel for the element.
-     > Parameters
-     - handler (function) handler for the event
-     = (object) @Element
-    \*/
-    /*\
-     * Element.untouchcancel
-     [ method ]
-     **
-     * Removes event handler for touchcancel for the element.
-     > Parameters
-     - handler (function) #optional handler for the event
-     = (object) @Element
-    \*/
-    for (var i = events.length; i--;) {
-        (function (eventName) {
-            R[eventName] = elproto[eventName] = function (fn, scope) {
-                if (R.is(fn, "function")) {
-                    this.events = this.events || [];
-                    this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)});
-                }
-                return this;
-            };
-            R["un" + eventName] = elproto["un" + eventName] = function (fn) {
-                var events = this.events || [],
-                    l = events.length;
-                while (l--){
-                    if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) {
-                        events[l].unbind();
-                        events.splice(l, 1);
-                        !events.length && delete this.events;
-                    }
-                }
-                return this;
-            };
-        })(events[i]);
-    }
-
-    /*\
-     * Element.data
-     [ method ]
-     **
-     * Adds or retrieves given value asociated with given key.
-     **
-     * See also @Element.removeData
-     > Parameters
-     - key (string) key to store data
-     - value (any) #optional value to store
-     = (object) @Element
-     * or, if value is not specified:
-     = (any) value
-     * or, if key and value are not specified:
-     = (object) Key/value pairs for all the data associated with the element.
-     > Usage
-     | for (var i = 0, i < 5, i++) {
-     |     paper.circle(10 + 15 * i, 10, 10)
-     |          .attr({fill: "#000"})
-     |          .data("i", i)
-     |          .click(function () {
-     |             alert(this.data("i"));
-     |          });
-     | }
-    \*/
-    elproto.data = function (key, value) {
-        var data = eldata[this.id] = eldata[this.id] || {};
-        if (arguments.length == 0) {
-            return data;
-        }
-        if (arguments.length == 1) {
-            if (R.is(key, "object")) {
-                for (var i in key) if (key[has](i)) {
-                    this.data(i, key[i]);
-                }
-                return this;
-            }
-            eve("raphael.data.get." + this.id, this, data[key], key);
-            return data[key];
-        }
-        data[key] = value;
-        eve("raphael.data.set." + this.id, this, value, key);
-        return this;
-    };
-    /*\
-     * Element.removeData
-     [ method ]
-     **
-     * Removes value associated with an element by given key.
-     * If key is not provided, removes all the data of the element.
-     > Parameters
-     - key (string) #optional key
-     = (object) @Element
-    \*/
-    elproto.removeData = function (key) {
-        if (key == null) {
-            eldata[this.id] = {};
-        } else {
-            eldata[this.id] && delete eldata[this.id][key];
-        }
-        return this;
-    };
-     /*\
-     * Element.getData
-     [ method ]
-     **
-     * Retrieves the element data
-     = (object) data
-    \*/
-    elproto.getData = function () {
-        return clone(eldata[this.id] || {});
-    };
-    /*\
-     * Element.hover
-     [ method ]
-     **
-     * Adds event handlers for hover for the element.
-     > Parameters
-     - f_in (function) handler for hover in
-     - f_out (function) handler for hover out
-     - icontext (object) #optional context for hover in handler
-     - ocontext (object) #optional context for hover out handler
-     = (object) @Element
-    \*/
-    elproto.hover = function (f_in, f_out, scope_in, scope_out) {
-        return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in);
-    };
-    /*\
-     * Element.unhover
-     [ method ]
-     **
-     * Removes event handlers for hover for the element.
-     > Parameters
-     - f_in (function) handler for hover in
-     - f_out (function) handler for hover out
-     = (object) @Element
-    \*/
-    elproto.unhover = function (f_in, f_out) {
-        return this.unmouseover(f_in).unmouseout(f_out);
-    };
-    var draggable = [];
-    /*\
-     * Element.drag
-     [ method ]
-     **
-     * Adds event handlers for drag of the element.
-     > Parameters
-     - onmove (function) handler for moving
-     - onstart (function) handler for drag start
-     - onend (function) handler for drag end
-     - mcontext (object) #optional context for moving handler
-     - scontext (object) #optional context for drag start handler
-     - econtext (object) #optional context for drag end handler
-     * Additionaly following `drag` events will be triggered: `drag.start.<id>` on start,
-     * `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element
-     * `drag.over.<id>` will be fired as well.
-     *
-     * Start event and start handler will be called in specified context or in context of the element with following parameters:
-     o x (number) x position of the mouse
-     o y (number) y position of the mouse
-     o event (object) DOM event object
-     * Move event and move handler will be called in specified context or in context of the element with following parameters:
-     o dx (number) shift by x from the start point
-     o dy (number) shift by y from the start point
-     o x (number) x position of the mouse
-     o y (number) y position of the mouse
-     o event (object) DOM event object
-     * End event and end handler will be called in specified context or in context of the element with following parameters:
-     o event (object) DOM event object
-     = (object) @Element
-    \*/
-    elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) {
-        function start(e) {
-            (e.originalEvent || e).preventDefault();
-            var x = e.clientX,
-                y = e.clientY,
-                scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
-                scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
-            this._drag.id = e.identifier;
-            if (supportsTouch && e.touches) {
-                var i = e.touches.length, touch;
-                while (i--) {
-                    touch = e.touches[i];
-                    this._drag.id = touch.identifier;
-                    if (touch.identifier == this._drag.id) {
-                        x = touch.clientX;
-                        y = touch.clientY;
-                        break;
-                    }
-                }
-            }
-            this._drag.x = x + scrollX;
-            this._drag.y = y + scrollY;
-            !drag.length && R.mousemove(dragMove).mouseup(dragUp);
-            drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope});
-            onstart && eve.on("raphael.drag.start." + this.id, onstart);
-            onmove && eve.on("raphael.drag.move." + this.id, onmove);
-            onend && eve.on("raphael.drag.end." + this.id, onend);
-            eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e);
-        }
-        this._drag = {};
-        draggable.push({el: this, start: start});
-        this.mousedown(start);
-        return this;
-    };
-    /*\
-     * Element.onDragOver
-     [ method ]
-     **
-     * Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id).
-     > Parameters
-     - f (function) handler for event, first argument would be the element you are dragging over
-    \*/
-    elproto.onDragOver = function (f) {
-        f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id);
-    };
-    /*\
-     * Element.undrag
-     [ method ]
-     **
-     * Removes all drag event handlers from given element.
-    \*/
-    elproto.undrag = function () {
-        var i = draggable.length;
-        while (i--) if (draggable[i].el == this) {
-            this.unmousedown(draggable[i].start);
-            draggable.splice(i, 1);
-            eve.unbind("raphael.drag.*." + this.id);
-        }
-        !draggable.length && R.unmousemove(dragMove).unmouseup(dragUp);
-        drag = [];
-    };
-    /*\
-     * Paper.circle
-     [ method ]
-     **
-     * Draws a circle.
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the centre
-     - y (number) y coordinate of the centre
-     - r (number) radius
-     = (object) Raphaël element object with type “circle”
-     **
-     > Usage
-     | var c = paper.circle(50, 50, 40);
-    \*/
-    paperproto.circle = function (x, y, r) {
-        var out = R._engine.circle(this, x || 0, y || 0, r || 0);
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.rect
-     [ method ]
-     *
-     * Draws a rectangle.
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the top left corner
-     - y (number) y coordinate of the top left corner
-     - width (number) width
-     - height (number) height
-     - r (number) #optional radius for rounded corners, default is 0
-     = (object) Raphaël element object with type “rect”
-     **
-     > Usage
-     | // regular rectangle
-     | var c = paper.rect(10, 10, 50, 50);
-     | // rectangle with rounded corners
-     | var c = paper.rect(40, 40, 50, 50, 10);
-    \*/
-    paperproto.rect = function (x, y, w, h, r) {
-        var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0);
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.ellipse
-     [ method ]
-     **
-     * Draws an ellipse.
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the centre
-     - y (number) y coordinate of the centre
-     - rx (number) horizontal radius
-     - ry (number) vertical radius
-     = (object) Raphaël element object with type “ellipse”
-     **
-     > Usage
-     | var c = paper.ellipse(50, 50, 40, 20);
-    \*/
-    paperproto.ellipse = function (x, y, rx, ry) {
-        var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0);
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.path
-     [ method ]
-     **
-     * Creates a path element by given path data string.
-     > Parameters
-     - pathString (string) #optional path string in SVG format.
-     * Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example:
-     | "M10,20L30,40"
-     * Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative.
-     *
-     # <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p>
-     # <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody>
-     # <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr>
-     # <tr><td>Z</td><td>closepath</td><td>(none)</td></tr>
-     # <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr>
-     # <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr>
-     # <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr>
-     # <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr>
-     # <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr>
-     # <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr>
-     # <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr>
-     # <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr>
-     # <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table>
-     * * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier.
-     * Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning.
-     > Usage
-     | var c = paper.path("M10 10L90 90");
-     | // draw a diagonal line:
-     | // move to 10,10, line to 90,90
-     * For example of path strings, check out these icons: http://raphaeljs.com/icons/
-    \*/
-    paperproto.path = function (pathString) {
-        pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E);
-        var out = R._engine.path(R.format[apply](R, arguments), this);
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.image
-     [ method ]
-     **
-     * Embeds an image into the surface.
-     **
-     > Parameters
-     **
-     - src (string) URI of the source image
-     - x (number) x coordinate position
-     - y (number) y coordinate position
-     - width (number) width of the image
-     - height (number) height of the image
-     = (object) Raphaël element object with type “image”
-     **
-     > Usage
-     | var c = paper.image("apple.png", 10, 10, 80, 80);
-    \*/
-    paperproto.image = function (src, x, y, w, h) {
-        var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.text
-     [ method ]
-     **
-     * Draws a text string. If you need line breaks, put “\n” in the string.
-     **
-     > Parameters
-     **
-     - x (number) x coordinate position
-     - y (number) y coordinate position
-     - text (string) The text string to draw
-     = (object) Raphaël element object with type “text”
-     **
-     > Usage
-     | var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!");
-    \*/
-    paperproto.text = function (x, y, text) {
-        var out = R._engine.text(this, x || 0, y || 0, Str(text));
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Paper.set
-     [ method ]
-     **
-     * Creates array-like object to keep and operate several elements at once.
-     * Warning: it doesn’t create any elements for itself in the page, it just groups existing elements.
-     * Sets act as pseudo elements — all methods available to an element can be used on a set.
-     = (object) array-like object that represents set of elements
-     **
-     > Usage
-     | var st = paper.set();
-     | st.push(
-     |     paper.circle(10, 10, 5),
-     |     paper.circle(30, 10, 5)
-     | );
-     | st.attr({fill: "red"}); // changes the fill of both circles
-    \*/
-    paperproto.set = function (itemsArray) {
-        !R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length));
-        var out = new Set(itemsArray);
-        this.__set__ && this.__set__.push(out);
-        out["paper"] = this;
-        out["type"] = "set";
-        return out;
-    };
-    /*\
-     * Paper.setStart
-     [ method ]
-     **
-     * Creates @Paper.set. All elements that will be created after calling this method and before calling
-     * @Paper.setFinish will be added to the set.
-     **
-     > Usage
-     | paper.setStart();
-     | paper.circle(10, 10, 5),
-     | paper.circle(30, 10, 5)
-     | var st = paper.setFinish();
-     | st.attr({fill: "red"}); // changes the fill of both circles
-    \*/
-    paperproto.setStart = function (set) {
-        this.__set__ = set || this.set();
-    };
-    /*\
-     * Paper.setFinish
-     [ method ]
-     **
-     * See @Paper.setStart. This method finishes catching and returns resulting set.
-     **
-     = (object) set
-    \*/
-    paperproto.setFinish = function (set) {
-        var out = this.__set__;
-        delete this.__set__;
-        return out;
-    };
-    /*\
-     * Paper.getSize
-     [ method ]
-     **
-     * Obtains current paper actual size.
-     **
-     = (object)
-     \*/
-    paperproto.getSize = function () {
-        var container = this.canvas.parentNode;
-        return {
-            width: container.offsetWidth,
-            height: container.offsetHeight
-                };
-        };
-    /*\
-     * Paper.setSize
-     [ method ]
-     **
-     * If you need to change dimensions of the canvas call this method
-     **
-     > Parameters
-     **
-     - width (number) new width of the canvas
-     - height (number) new height of the canvas
-    \*/
-    paperproto.setSize = function (width, height) {
-        return R._engine.setSize.call(this, width, height);
-    };
-    /*\
-     * Paper.setViewBox
-     [ method ]
-     **
-     * Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by
-     * specifying new boundaries.
-     **
-     > Parameters
-     **
-     - x (number) new x position, default is `0`
-     - y (number) new y position, default is `0`
-     - w (number) new width of the canvas
-     - h (number) new height of the canvas
-     - fit (boolean) `true` if you want graphics to fit into new boundary box
-    \*/
-    paperproto.setViewBox = function (x, y, w, h, fit) {
-        return R._engine.setViewBox.call(this, x, y, w, h, fit);
-    };
-    /*\
-     * Paper.top
-     [ property ]
-     **
-     * Points to the topmost element on the paper
-    \*/
-    /*\
-     * Paper.bottom
-     [ property ]
-     **
-     * Points to the bottom element on the paper
-    \*/
-    paperproto.top = paperproto.bottom = null;
-    /*\
-     * Paper.raphael
-     [ property ]
-     **
-     * Points to the @Raphael object/function
-    \*/
-    paperproto.raphael = R;
-    var getOffset = function (elem) {
-        var box = elem.getBoundingClientRect(),
-            doc = elem.ownerDocument,
-            body = doc.body,
-            docElem = doc.documentElement,
-            clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
-            top  = box.top  + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop,
-            left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft;
-        return {
-            y: top,
-            x: left
-        };
-    };
-    /*\
-     * Paper.getElementByPoint
-     [ method ]
-     **
-     * Returns you topmost element under given point.
-     **
-     = (object) Raphaël element object
-     > Parameters
-     **
-     - x (number) x coordinate from the top left corner of the window
-     - y (number) y coordinate from the top left corner of the window
-     > Usage
-     | paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"});
-    \*/
-    paperproto.getElementByPoint = function (x, y) {
-        var paper = this,
-            svg = paper.canvas,
-            target = g.doc.elementFromPoint(x, y);
-        if (g.win.opera && target.tagName == "svg") {
-            var so = getOffset(svg),
-                sr = svg.createSVGRect();
-            sr.x = x - so.x;
-            sr.y = y - so.y;
-            sr.width = sr.height = 1;
-            var hits = svg.getIntersectionList(sr, null);
-            if (hits.length) {
-                target = hits[hits.length - 1];
-            }
-        }
-        if (!target) {
-            return null;
-        }
-        while (target.parentNode && target != svg.parentNode && !target.raphael) {
-            target = target.parentNode;
-        }
-        target == paper.canvas.parentNode && (target = svg);
-        target = target && target.raphael ? paper.getById(target.raphaelid) : null;
-        return target;
-    };
-
-    /*\
-     * Paper.getElementsByBBox
-     [ method ]
-     **
-     * Returns set of elements that have an intersecting bounding box
-     **
-     > Parameters
-     **
-     - bbox (object) bbox to check with
-     = (object) @Set
-     \*/
-    paperproto.getElementsByBBox = function (bbox) {
-        var set = this.set();
-        this.forEach(function (el) {
-            if (R.isBBoxIntersect(el.getBBox(), bbox)) {
-                set.push(el);
-            }
-        });
-        return set;
-    };
-
-    /*\
-     * Paper.getById
-     [ method ]
-     **
-     * Returns you element by its internal ID.
-     **
-     > Parameters
-     **
-     - id (number) id
-     = (object) Raphaël element object
-    \*/
-    paperproto.getById = function (id) {
-        var bot = this.bottom;
-        while (bot) {
-            if (bot.id == id) {
-                return bot;
-            }
-            bot = bot.next;
-        }
-        return null;
-    };
-    /*\
-     * Paper.forEach
-     [ method ]
-     **
-     * Executes given function for each element on the paper
-     *
-     * If callback function returns `false` it will stop loop running.
-     **
-     > Parameters
-     **
-     - callback (function) function to run
-     - thisArg (object) context object for the callback
-     = (object) Paper object
-     > Usage
-     | paper.forEach(function (el) {
-     |     el.attr({ stroke: "blue" });
-     | });
-    \*/
-    paperproto.forEach = function (callback, thisArg) {
-        var bot = this.bottom;
-        while (bot) {
-            if (callback.call(thisArg, bot) === false) {
-                return this;
-            }
-            bot = bot.next;
-        }
-        return this;
-    };
-    /*\
-     * Paper.getElementsByPoint
-     [ method ]
-     **
-     * Returns set of elements that have common point inside
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the point
-     - y (number) y coordinate of the point
-     = (object) @Set
-    \*/
-    paperproto.getElementsByPoint = function (x, y) {
-        var set = this.set();
-        this.forEach(function (el) {
-            if (el.isPointInside(x, y)) {
-                set.push(el);
-            }
-        });
-        return set;
-    };
-    function x_y() {
-        return this.x + S + this.y;
-    }
-    function x_y_w_h() {
-        return this.x + S + this.y + S + this.width + " \xd7 " + this.height;
-    }
-    /*\
-     * Element.isPointInside
-     [ method ]
-     **
-     * Determine if given point is inside this element’s shape
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the point
-     - y (number) y coordinate of the point
-     = (boolean) `true` if point inside the shape
-    \*/
-    elproto.isPointInside = function (x, y) {
-        var rp = this.realPath = getPath[this.type](this);
-        if (this.attr('transform') && this.attr('transform').length) {
-            rp = R.transformPath(rp, this.attr('transform'));
-        }
-        return R.isPointInsidePath(rp, x, y);
-    };
-    /*\
-     * Element.getBBox
-     [ method ]
-     **
-     * Return bounding box for a given element
-     **
-     > Parameters
-     **
-     - isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`.
-     = (object) Bounding box object:
-     o {
-     o     x: (number) top left corner x
-     o     y: (number) top left corner y
-     o     x2: (number) bottom right corner x
-     o     y2: (number) bottom right corner y
-     o     width: (number) width
-     o     height: (number) height
-     o }
-    \*/
-    elproto.getBBox = function (isWithoutTransform) {
-        if (this.removed) {
-            return {};
-        }
-        var _ = this._;
-        if (isWithoutTransform) {
-            if (_.dirty || !_.bboxwt) {
-                this.realPath = getPath[this.type](this);
-                _.bboxwt = pathDimensions(this.realPath);
-                _.bboxwt.toString = x_y_w_h;
-                _.dirty = 0;
-            }
-            return _.bboxwt;
-        }
-        if (_.dirty || _.dirtyT || !_.bbox) {
-            if (_.dirty || !this.realPath) {
-                _.bboxwt = 0;
-                this.realPath = getPath[this.type](this);
-            }
-            _.bbox = pathDimensions(mapPath(this.realPath, this.matrix));
-            _.bbox.toString = x_y_w_h;
-            _.dirty = _.dirtyT = 0;
-        }
-        return _.bbox;
-    };
-    /*\
-     * Element.clone
-     [ method ]
-     **
-     = (object) clone of a given element
-     **
-    \*/
-    elproto.clone = function () {
-        if (this.removed) {
-            return null;
-        }
-        var out = this.paper[this.type]().attr(this.attr());
-        this.__set__ && this.__set__.push(out);
-        return out;
-    };
-    /*\
-     * Element.glow
-     [ method ]
-     **
-     * Return set of elements that create glow-like effect around given element. See @Paper.set.
-     *
-     * Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself.
-     **
-     > Parameters
-     **
-     - glow (object) #optional parameters object with all properties optional:
-     o {
-     o     width (number) size of the glow, default is `10`
-     o     fill (boolean) will it be filled, default is `false`
-     o     opacity (number) opacity, default is `0.5`
-     o     offsetx (number) horizontal offset, default is `0`
-     o     offsety (number) vertical offset, default is `0`
-     o     color (string) glow colour, default is `black`
-     o }
-     = (object) @Paper.set of elements that represents glow
-    \*/
-    elproto.glow = function (glow) {
-        if (this.type == "text") {
-            return null;
-        }
-        glow = glow || {};
-        var s = {
-            width: (glow.width || 10) + (+this.attr("stroke-width") || 1),
-            fill: glow.fill || false,
-            opacity: glow.opacity || .5,
-            offsetx: glow.offsetx || 0,
-            offsety: glow.offsety || 0,
-            color: glow.color || "#000"
-        },
-            c = s.width / 2,
-            r = this.paper,
-            out = r.set(),
-            path = this.realPath || getPath[this.type](this);
-        path = this.matrix ? mapPath(path, this.matrix) : path;
-        for (var i = 1; i < c + 1; i++) {
-            out.push(r.path(path).attr({
-                stroke: s.color,
-                fill: s.fill ? s.color : "none",
-                "stroke-linejoin": "round",
-                "stroke-linecap": "round",
-                "stroke-width": +(s.width / c * i).toFixed(3),
-                opacity: +(s.opacity / c).toFixed(3)
-            }));
-        }
-        return out.insertBefore(this).translate(s.offsetx, s.offsety);
-    };
-    var curveslengths = {},
-    getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
-        if (length == null) {
-            return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
-        } else {
-            return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
-        }
-    },
-    getLengthFactory = function (istotal, subpath) {
-        return function (path, length, onlystart) {
-            path = path2curve(path);
-            var x, y, p, l, sp = "", subpaths = {}, point,
-                len = 0;
-            for (var i = 0, ii = path.length; i < ii; i++) {
-                p = path[i];
-                if (p[0] == "M") {
-                    x = +p[1];
-                    y = +p[2];
-                } else {
-                    l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
-                    if (len + l > length) {
-                        if (subpath && !subpaths.start) {
-                            point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
-                            sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
-                            if (onlystart) {return sp;}
-                            subpaths.start = sp;
-                            sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join();
-                            len += l;
-                            x = +p[5];
-                            y = +p[6];
-                            continue;
-                        }
-                        if (!istotal && !subpath) {
-                            point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
-                            return {x: point.x, y: point.y, alpha: point.alpha};
-                        }
-                    }
-                    len += l;
-                    x = +p[5];
-                    y = +p[6];
-                }
-                sp += p.shift() + p;
-            }
-            subpaths.end = sp;
-            point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
-            point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
-            return point;
-        };
-    };
-    var getTotalLength = getLengthFactory(1),
-        getPointAtLength = getLengthFactory(),
-        getSubpathsAtLength = getLengthFactory(0, 1);
-    /*\
-     * Raphael.getTotalLength
-     [ method ]
-     **
-     * Returns length of the given path in pixels.
-     **
-     > Parameters
-     **
-     - path (string) SVG path string.
-     **
-     = (number) length.
-    \*/
-    R.getTotalLength = getTotalLength;
-    /*\
-     * Raphael.getPointAtLength
-     [ method ]
-     **
-     * Return coordinates of the point located at the given length on the given path.
-     **
-     > Parameters
-     **
-     - path (string) SVG path string
-     - length (number)
-     **
-     = (object) representation of the point:
-     o {
-     o     x: (number) x coordinate
-     o     y: (number) y coordinate
-     o     alpha: (number) angle of derivative
-     o }
-    \*/
-    R.getPointAtLength = getPointAtLength;
-    /*\
-     * Raphael.getSubpath
-     [ method ]
-     **
-     * Return subpath of a given path from given length to given length.
-     **
-     > Parameters
-     **
-     - path (string) SVG path string
-     - from (number) position of the start of the segment
-     - to (number) position of the end of the segment
-     **
-     = (string) pathstring for the segment
-    \*/
-    R.getSubpath = function (path, from, to) {
-        if (this.getTotalLength(path) - to < 1e-6) {
-            return getSubpathsAtLength(path, from).end;
-        }
-        var a = getSubpathsAtLength(path, to, 1);
-        return from ? getSubpathsAtLength(a, from).end : a;
-    };
-    /*\
-     * Element.getTotalLength
-     [ method ]
-     **
-     * Returns length of the path in pixels. Only works for element of “path” type.
-     = (number) length.
-    \*/
-    elproto.getTotalLength = function () {
-        var path = this.getPath();
-        if (!path) {
-            return;
-        }
-
-        if (this.node.getTotalLength) {
-            return this.node.getTotalLength();
-        }
-
-        return getTotalLength(path);
-    };
-    /*\
-     * Element.getPointAtLength
-     [ method ]
-     **
-     * Return coordinates of the point located at the given length on the given path. Only works for element of “path” type.
-     **
-     > Parameters
-     **
-     - length (number)
-     **
-     = (object) representation of the point:
-     o {
-     o     x: (number) x coordinate
-     o     y: (number) y coordinate
-     o     alpha: (number) angle of derivative
-     o }
-    \*/
-    elproto.getPointAtLength = function (length) {
-        var path = this.getPath();
-        if (!path) {
-            return;
-        }
-
-        return getPointAtLength(path, length);
-    };
-    /*\
-     * Element.getPath
-     [ method ]
-     **
-     * Returns path of the element. Only works for elements of “path” type and simple elements like circle.
-     = (object) path
-     **
-    \*/
-    elproto.getPath = function () {
-        var path,
-            getPath = R._getPath[this.type];
-
-        if (this.type == "text" || this.type == "set") {
-            return;
-        }
-
-        if (getPath) {
-            path = getPath(this);
-        }
-
-        return path;
-    };
-    /*\
-     * Element.getSubpath
-     [ method ]
-     **
-     * Return subpath of a given element from given length to given length. Only works for element of “path” type.
-     **
-     > Parameters
-     **
-     - from (number) position of the start of the segment
-     - to (number) position of the end of the segment
-     **
-     = (string) pathstring for the segment
-    \*/
-    elproto.getSubpath = function (from, to) {
-        var path = this.getPath();
-        if (!path) {
-            return;
-        }
-
-        return R.getSubpath(path, from, to);
-    };
-    /*\
-     * Raphael.easing_formulas
-     [ property ]
-     **
-     * Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing:
-     # <ul>
-     #     <li>“linear”</li>
-     #     <li>“&lt;” or “easeIn” or “ease-in”</li>
-     #     <li>“>” or “easeOut” or “ease-out”</li>
-     #     <li>“&lt;>” or “easeInOut” or “ease-in-out”</li>
-     #     <li>“backIn” or “back-in”</li>
-     #     <li>“backOut” or “back-out”</li>
-     #     <li>“elastic”</li>
-     #     <li>“bounce”</li>
-     # </ul>
-     # <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p>
-    \*/
-    var ef = R.easing_formulas = {
-        linear: function (n) {
-            return n;
-        },
-        "<": function (n) {
-            return pow(n, 1.7);
-        },
-        ">": function (n) {
-            return pow(n, .48);
-        },
-        "<>": function (n) {
-            var q = .48 - n / 1.04,
-                Q = math.sqrt(.1734 + q * q),
-                x = Q - q,
-                X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
-                y = -Q - q,
-                Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
-                t = X + Y + .5;
-            return (1 - t) * 3 * t * t + t * t * t;
-        },
-        backIn: function (n) {
-            var s = 1.70158;
-            return n * n * ((s + 1) * n - s);
-        },
-        backOut: function (n) {
-            n = n - 1;
-            var s = 1.70158;
-            return n * n * ((s + 1) * n + s) + 1;
-        },
-        elastic: function (n) {
-            if (n == !!n) {
-                return n;
-            }
-            return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1;
-        },
-        bounce: function (n) {
-            var s = 7.5625,
-                p = 2.75,
-                l;
-            if (n < (1 / p)) {
-                l = s * n * n;
-            } else {
-                if (n < (2 / p)) {
-                    n -= (1.5 / p);
-                    l = s * n * n + .75;
-                } else {
-                    if (n < (2.5 / p)) {
-                        n -= (2.25 / p);
-                        l = s * n * n + .9375;
-                    } else {
-                        n -= (2.625 / p);
-                        l = s * n * n + .984375;
-                    }
-                }
-            }
-            return l;
-        }
-    };
-    ef.easeIn = ef["ease-in"] = ef["<"];
-    ef.easeOut = ef["ease-out"] = ef[">"];
-    ef.easeInOut = ef["ease-in-out"] = ef["<>"];
-    ef["back-in"] = ef.backIn;
-    ef["back-out"] = ef.backOut;
-
-    var animationElements = [],
-        requestAnimFrame = window.requestAnimationFrame       ||
-                           window.webkitRequestAnimationFrame ||
-                           window.mozRequestAnimationFrame    ||
-                           window.oRequestAnimationFrame      ||
-                           window.msRequestAnimationFrame     ||
-                           function (callback) {
-                               setTimeout(callback, 16);
-                           },
-        animation = function () {
-            var Now = +new Date,
-                l = 0;
-            for (; l < animationElements.length; l++) {
-                var e = animationElements[l];
-                if (e.el.removed || e.paused) {
-                    continue;
-                }
-                var time = Now - e.start,
-                    ms = e.ms,
-                    easing = e.easing,
-                    from = e.from,
-                    diff = e.diff,
-                    to = e.to,
-                    t = e.t,
-                    that = e.el,
-                    set = {},
-                    now,
-                    init = {},
-                    key;
-                if (e.initstatus) {
-                    time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms;
-                    e.status = e.initstatus;
-                    delete e.initstatus;
-                    e.stop && animationElements.splice(l--, 1);
-                } else {
-                    e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top;
-                }
-                if (time < 0) {
-                    continue;
-                }
-                if (time < ms) {
-                    var pos = easing(time / ms);
-                    for (var attr in from) if (from[has](attr)) {
-                        switch (availableAnimAttrs[attr]) {
-                            case nu:
-                                now = +from[attr] + pos * ms * diff[attr];
-                                break;
-                            case "colour":
-                                now = "rgb(" + [
-                                    upto255(round(from[attr].r + pos * ms * diff[attr].r)),
-                                    upto255(round(from[attr].g + pos * ms * diff[attr].g)),
-                                    upto255(round(from[attr].b + pos * ms * diff[attr].b))
-                                ].join(",") + ")";
-                                break;
-                            case "path":
-                                now = [];
-                                for (var i = 0, ii = from[attr].length; i < ii; i++) {
-                                    now[i] = [from[attr][i][0]];
-                                    for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
-                                        now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
-                                    }
-                                    now[i] = now[i].join(S);
-                                }
-                                now = now.join(S);
-                                break;
-                            case "transform":
-                                if (diff[attr].real) {
-                                    now = [];
-                                    for (i = 0, ii = from[attr].length; i < ii; i++) {
-                                        now[i] = [from[attr][i][0]];
-                                        for (j = 1, jj = from[attr][i].length; j < jj; j++) {
-                                            now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j];
-                                        }
-                                    }
-                                } else {
-                                    var get = function (i) {
-                                        return +from[attr][i] + pos * ms * diff[attr][i];
-                                    };
-                                    // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]];
-                                    now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]];
-                                }
-                                break;
-                            case "csv":
-                                if (attr == "clip-rect") {
-                                    now = [];
-                                    i = 4;
-                                    while (i--) {
-                                        now[i] = +from[attr][i] + pos * ms * diff[attr][i];
-                                    }
-                                }
-                                break;
-                            default:
-                                var from2 = [][concat](from[attr]);
-                                now = [];
-                                i = that.paper.customAttributes[attr].length;
-                                while (i--) {
-                                    now[i] = +from2[i] + pos * ms * diff[attr][i];
-                                }
-                                break;
-                        }
-                        set[attr] = now;
-                    }
-                    that.attr(set);
-                    (function (id, that, anim) {
-                        setTimeout(function () {
-                            eve("raphael.anim.frame." + id, that, anim);
-                        });
-                    })(that.id, that, e.anim);
-                } else {
-                    (function(f, el, a) {
-                        setTimeout(function() {
-                            eve("raphael.anim.frame." + el.id, el, a);
-                            eve("raphael.anim.finish." + el.id, el, a);
-                            R.is(f, "function") && f.call(el);
-                        });
-                    })(e.callback, that, e.anim);
-                    that.attr(to);
-                    animationElements.splice(l--, 1);
-                    if (e.repeat > 1 && !e.next) {
-                        for (key in to) if (to[has](key)) {
-                            init[key] = e.totalOrigin[key];
-                        }
-                        e.el.attr(init);
-                        runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1);
-                    }
-                    if (e.next && !e.stop) {
-                        runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat);
-                    }
-                }
-            }
-            R.svg && that && that.paper && that.paper.safari();
-            animationElements.length && requestAnimFrame(animation);
-        },
-        upto255 = function (color) {
-            return color > 255 ? 255 : color < 0 ? 0 : color;
-        };
-    /*\
-     * Element.animateWith
-     [ method ]
-     **
-     * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element.
-     **
-     > Parameters
-     **
-     - el (object) element to sync with
-     - anim (object) animation to sync with
-     - params (object) #optional final attributes for the element, see also @Element.attr
-     - ms (number) #optional number of milliseconds for animation to run
-     - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
-     - callback (function) #optional callback function. Will be called at the end of animation.
-     * or
-     - element (object) element to sync with
-     - anim (object) animation to sync with
-     - animation (object) #optional animation object, see @Raphael.animation
-     **
-     = (object) original element
-    \*/
-    elproto.animateWith = function (el, anim, params, ms, easing, callback) {
-        var element = this;
-        if (element.removed) {
-            callback && callback.call(element);
-            return element;
-        }
-        var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback),
-            x, y;
-        runAnimation(a, element, a.percents[0], null, element.attr());
-        for (var i = 0, ii = animationElements.length; i < ii; i++) {
-            if (animationElements[i].anim == anim && animationElements[i].el == el) {
-                animationElements[ii - 1].start = animationElements[i].start;
-                break;
-            }
-        }
-        return element;
-        //
-        //
-        // var a = params ? R.animation(params, ms, easing, callback) : anim,
-        //     status = element.status(anim);
-        // return this.animate(a).status(a, status * anim.ms / a.ms);
-    };
-    function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) {
-        var cx = 3 * p1x,
-            bx = 3 * (p2x - p1x) - cx,
-            ax = 1 - cx - bx,
-            cy = 3 * p1y,
-            by = 3 * (p2y - p1y) - cy,
-            ay = 1 - cy - by;
-        function sampleCurveX(t) {
-            return ((ax * t + bx) * t + cx) * t;
-        }
-        function solve(x, epsilon) {
-            var t = solveCurveX(x, epsilon);
-            return ((ay * t + by) * t + cy) * t;
-        }
-        function solveCurveX(x, epsilon) {
-            var t0, t1, t2, x2, d2, i;
-            for(t2 = x, i = 0; i < 8; i++) {
-                x2 = sampleCurveX(t2) - x;
-                if (abs(x2) < epsilon) {
-                    return t2;
-                }
-                d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
-                if (abs(d2) < 1e-6) {
-                    break;
-                }
-                t2 = t2 - x2 / d2;
-            }
-            t0 = 0;
-            t1 = 1;
-            t2 = x;
-            if (t2 < t0) {
-                return t0;
-            }
-            if (t2 > t1) {
-                return t1;
-            }
-            while (t0 < t1) {
-                x2 = sampleCurveX(t2);
-                if (abs(x2 - x) < epsilon) {
-                    return t2;
-                }
-                if (x > x2) {
-                    t0 = t2;
-                } else {
-                    t1 = t2;
-                }
-                t2 = (t1 - t0) / 2 + t0;
-            }
-            return t2;
-        }
-        return solve(t, 1 / (200 * duration));
-    }
-    elproto.onAnimation = function (f) {
-        f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id);
-        return this;
-    };
-    function Animation(anim, ms) {
-        var percents = [],
-            newAnim = {};
-        this.ms = ms;
-        this.times = 1;
-        if (anim) {
-            for (var attr in anim) if (anim[has](attr)) {
-                newAnim[toFloat(attr)] = anim[attr];
-                percents.push(toFloat(attr));
-            }
-            percents.sort(sortByNumber);
-        }
-        this.anim = newAnim;
-        this.top = percents[percents.length - 1];
-        this.percents = percents;
-    }
-    /*\
-     * Animation.delay
-     [ method ]
-     **
-     * Creates a copy of existing animation object with given delay.
-     **
-     > Parameters
-     **
-     - delay (number) number of ms to pass between animation start and actual animation
-     **
-     = (object) new altered Animation object
-     | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3);
-     | circle1.animate(anim); // run the given animation immediately
-     | circle2.animate(anim.delay(500)); // run the given animation after 500 ms
-    \*/
-    Animation.prototype.delay = function (delay) {
-        var a = new Animation(this.anim, this.ms);
-        a.times = this.times;
-        a.del = +delay || 0;
-        return a;
-    };
-    /*\
-     * Animation.repeat
-     [ method ]
-     **
-     * Creates a copy of existing animation object with given repetition.
-     **
-     > Parameters
-     **
-     - repeat (number) number iterations of animation. For infinite animation pass `Infinity`
-     **
-     = (object) new altered Animation object
-    \*/
-    Animation.prototype.repeat = function (times) {
-        var a = new Animation(this.anim, this.ms);
-        a.del = this.del;
-        a.times = math.floor(mmax(times, 0)) || 1;
-        return a;
-    };
-    function runAnimation(anim, element, percent, status, totalOrigin, times) {
-        percent = toFloat(percent);
-        var params,
-            isInAnim,
-            isInAnimSet,
-            percents = [],
-            next,
-            prev,
-            timestamp,
-            ms = anim.ms,
-            from = {},
-            to = {},
-            diff = {};
-        if (status) {
-            for (i = 0, ii = animationElements.length; i < ii; i++) {
-                var e = animationElements[i];
-                if (e.el.id == element.id && e.anim == anim) {
-                    if (e.percent != percent) {
-                        animationElements.splice(i, 1);
-                        isInAnimSet = 1;
-                    } else {
-                        isInAnim = e;
-                    }
-                    element.attr(e.totalOrigin);
-                    break;
-                }
-            }
-        } else {
-            status = +to; // NaN
-        }
-        for (var i = 0, ii = anim.percents.length; i < ii; i++) {
-            if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) {
-                percent = anim.percents[i];
-                prev = anim.percents[i - 1] || 0;
-                ms = ms / anim.top * (percent - prev);
-                next = anim.percents[i + 1];
-                params = anim.anim[percent];
-                break;
-            } else if (status) {
-                element.attr(anim.anim[anim.percents[i]]);
-            }
-        }
-        if (!params) {
-            return;
-        }
-        if (!isInAnim) {
-            for (var attr in params) if (params[has](attr)) {
-                if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) {
-                    from[attr] = element.attr(attr);
-                    (from[attr] == null) && (from[attr] = availableAttrs[attr]);
-                    to[attr] = params[attr];
-                    switch (availableAnimAttrs[attr]) {
-                        case nu:
-                            diff[attr] = (to[attr] - from[attr]) / ms;
-                            break;
-                        case "colour":
-                            from[attr] = R.getRGB(from[attr]);
-                            var toColour = R.getRGB(to[attr]);
-                            diff[attr] = {
-                                r: (toColour.r - from[attr].r) / ms,
-                                g: (toColour.g - from[attr].g) / ms,
-                                b: (toColour.b - from[attr].b) / ms
-                            };
-                            break;
-                        case "path":
-                            var pathes = path2curve(from[attr], to[attr]),
-                                toPath = pathes[1];
-                            from[attr] = pathes[0];
-                            diff[attr] = [];
-                            for (i = 0, ii = from[attr].length; i < ii; i++) {
-                                diff[attr][i] = [0];
-                                for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
-                                    diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
-                                }
-                            }
-                            break;
-                        case "transform":
-                            var _ = element._,
-                                eq = equaliseTransform(_[attr], to[attr]);
-                            if (eq) {
-                                from[attr] = eq.from;
-                                to[attr] = eq.to;
-                                diff[attr] = [];
-                                diff[attr].real = true;
-                                for (i = 0, ii = from[attr].length; i < ii; i++) {
-                                    diff[attr][i] = [from[attr][i][0]];
-                                    for (j = 1, jj = from[attr][i].length; j < jj; j++) {
-                                        diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms;
-                                    }
-                                }
-                            } else {
-                                var m = (element.matrix || new Matrix),
-                                    to2 = {
-                                        _: {transform: _.transform},
-                                        getBBox: function () {
-                                            return element.getBBox(1);
-                                        }
-                                    };
-                                from[attr] = [
-                                    m.a,
-                                    m.b,
-                                    m.c,
-                                    m.d,
-                                    m.e,
-                                    m.f
-                                ];
-                                extractTransform(to2, to[attr]);
-                                to[attr] = to2._.transform;
-                                diff[attr] = [
-                                    (to2.matrix.a - m.a) / ms,
-                                    (to2.matrix.b - m.b) / ms,
-                                    (to2.matrix.c - m.c) / ms,
-                                    (to2.matrix.d - m.d) / ms,
-                                    (to2.matrix.e - m.e) / ms,
-                                    (to2.matrix.f - m.f) / ms
-                                ];
-                                // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy];
-                                // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }};
-                                // extractTransform(to2, to[attr]);
-                                // diff[attr] = [
-                                //     (to2._.sx - _.sx) / ms,
-                                //     (to2._.sy - _.sy) / ms,
-                                //     (to2._.deg - _.deg) / ms,
-                                //     (to2._.dx - _.dx) / ms,
-                                //     (to2._.dy - _.dy) / ms
-                                // ];
-                            }
-                            break;
-                        case "csv":
-                            var values = Str(params[attr])[split](separator),
-                                from2 = Str(from[attr])[split](separator);
-                            if (attr == "clip-rect") {
-                                from[attr] = from2;
-                                diff[attr] = [];
-                                i = from2.length;
-                                while (i--) {
-                                    diff[attr][i] = (values[i] - from[attr][i]) / ms;
-                                }
-                            }
-                            to[attr] = values;
-                            break;
-                        default:
-                            values = [][concat](params[attr]);
-                            from2 = [][concat](from[attr]);
-                            diff[attr] = [];
-                            i = element.paper.customAttributes[attr].length;
-                            while (i--) {
-                                diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms;
-                            }
-                            break;
-                    }
-                }
-            }
-            var easing = params.easing,
-                easyeasy = R.easing_formulas[easing];
-            if (!easyeasy) {
-                easyeasy = Str(easing).match(bezierrg);
-                if (easyeasy && easyeasy.length == 5) {
-                    var curve = easyeasy;
-                    easyeasy = function (t) {
-                        return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms);
-                    };
-                } else {
-                    easyeasy = pipe;
-                }
-            }
-            timestamp = params.start || anim.start || +new Date;
-            e = {
-                anim: anim,
-                percent: percent,
-                timestamp: timestamp,
-                start: timestamp + (anim.del || 0),
-                status: 0,
-                initstatus: status || 0,
-                stop: false,
-                ms: ms,
-                easing: easyeasy,
-                from: from,
-                diff: diff,
-                to: to,
-                el: element,
-                callback: params.callback,
-                prev: prev,
-                next: next,
-                repeat: times || anim.times,
-                origin: element.attr(),
-                totalOrigin: totalOrigin
-            };
-            animationElements.push(e);
-            if (status && !isInAnim && !isInAnimSet) {
-                e.stop = true;
-                e.start = new Date - ms * status;
-                if (animationElements.length == 1) {
-                    return animation();
-                }
-            }
-            if (isInAnimSet) {
-                e.start = new Date - e.ms * status;
-            }
-            animationElements.length == 1 && requestAnimFrame(animation);
-        } else {
-            isInAnim.initstatus = status;
-            isInAnim.start = new Date - isInAnim.ms * status;
-        }
-        eve("raphael.anim.start." + element.id, element, anim);
-    }
-    /*\
-     * Raphael.animation
-     [ method ]
-     **
-     * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods.
-     * See also @Animation.delay and @Animation.repeat methods.
-     **
-     > Parameters
-     **
-     - params (object) final attributes for the element, see also @Element.attr
-     - ms (number) number of milliseconds for animation to run
-     - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
-     - callback (function) #optional callback function. Will be called at the end of animation.
-     **
-     = (object) @Animation
-    \*/
-    R.animation = function (params, ms, easing, callback) {
-        if (params instanceof Animation) {
-            return params;
-        }
-        if (R.is(easing, "function") || !easing) {
-            callback = callback || easing || null;
-            easing = null;
-        }
-        params = Object(params);
-        ms = +ms || 0;
-        var p = {},
-            json,
-            attr;
-        for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) {
-            json = true;
-            p[attr] = params[attr];
-        }
-        if (!json) {
-            // if percent-like syntax is used and end-of-all animation callback used
-            if(callback){
-                // find the last one
-                var lastKey = 0;
-                for(var i in params){
-                    var percent = toInt(i);
-                    if(params[has](i) && percent > lastKey){
-                        lastKey = percent;
-                    }
-                }
-                lastKey += '%';
-                // if already defined callback in the last keyframe, skip
-                !params[lastKey].callback && (params[lastKey].callback = callback);
-            }
-          return new Animation(params, ms);
-        } else {
-            easing && (p.easing = easing);
-            callback && (p.callback = callback);
-            return new Animation({100: p}, ms);
-        }
-    };
-    /*\
-     * Element.animate
-     [ method ]
-     **
-     * Creates and starts animation for given element.
-     **
-     > Parameters
-     **
-     - params (object) final attributes for the element, see also @Element.attr
-     - ms (number) number of milliseconds for animation to run
-     - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
-     - callback (function) #optional callback function. Will be called at the end of animation.
-     * or
-     - animation (object) animation object, see @Raphael.animation
-     **
-     = (object) original element
-    \*/
-    elproto.animate = function (params, ms, easing, callback) {
-        var element = this;
-        if (element.removed) {
-            callback && callback.call(element);
-            return element;
-        }
-        var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback);
-        runAnimation(anim, element, anim.percents[0], null, element.attr());
-        return element;
-    };
-    /*\
-     * Element.setTime
-     [ method ]
-     **
-     * Sets the status of animation of the element in milliseconds. Similar to @Element.status method.
-     **
-     > Parameters
-     **
-     - anim (object) animation object
-     - value (number) number of milliseconds from the beginning of the animation
-     **
-     = (object) original element if `value` is specified
-     * Note, that during animation following events are triggered:
-     *
-     * On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`.
-    \*/
-    elproto.setTime = function (anim, value) {
-        if (anim && value != null) {
-            this.status(anim, mmin(value, anim.ms) / anim.ms);
-        }
-        return this;
-    };
-    /*\
-     * Element.status
-     [ method ]
-     **
-     * Gets or sets the status of animation of the element.
-     **
-     > Parameters
-     **
-     - anim (object) #optional animation object
-     - value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position.
-     **
-     = (number) status
-     * or
-     = (array) status if `anim` is not specified. Array of objects in format:
-     o {
-     o     anim: (object) animation object
-     o     status: (number) status
-     o }
-     * or
-     = (object) original element if `value` is specified
-    \*/
-    elproto.status = function (anim, value) {
-        var out = [],
-            i = 0,
-            len,
-            e;
-        if (value != null) {
-            runAnimation(anim, this, -1, mmin(value, 1));
-            return this;
-        } else {
-            len = animationElements.length;
-            for (; i < len; i++) {
-                e = animationElements[i];
-                if (e.el.id == this.id && (!anim || e.anim == anim)) {
-                    if (anim) {
-                        return e.status;
-                    }
-                    out.push({
-                        anim: e.anim,
-                        status: e.status
-                    });
-                }
-            }
-            if (anim) {
-                return 0;
-            }
-            return out;
-        }
-    };
-    /*\
-     * Element.pause
-     [ method ]
-     **
-     * Stops animation of the element with ability to resume it later on.
-     **
-     > Parameters
-     **
-     - anim (object) #optional animation object
-     **
-     = (object) original element
-    \*/
-    elproto.pause = function (anim) {
-        for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
-            if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) {
-                animationElements[i].paused = true;
-            }
-        }
-        return this;
-    };
-    /*\
-     * Element.resume
-     [ method ]
-     **
-     * Resumes animation if it was paused with @Element.pause method.
-     **
-     > Parameters
-     **
-     - anim (object) #optional animation object
-     **
-     = (object) original element
-    \*/
-    elproto.resume = function (anim) {
-        for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
-            var e = animationElements[i];
-            if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) {
-                delete e.paused;
-                this.status(e.anim, e.status);
-            }
-        }
-        return this;
-    };
-    /*\
-     * Element.stop
-     [ method ]
-     **
-     * Stops animation of the element.
-     **
-     > Parameters
-     **
-     - anim (object) #optional animation object
-     **
-     = (object) original element
-    \*/
-    elproto.stop = function (anim) {
-        for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
-            if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) {
-                animationElements.splice(i--, 1);
-            }
-        }
-        return this;
-    };
-    function stopAnimation(paper) {
-        for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) {
-            animationElements.splice(i--, 1);
-        }
-    }
-    eve.on("raphael.remove", stopAnimation);
-    eve.on("raphael.clear", stopAnimation);
-    elproto.toString = function () {
-        return "Rapha\xebl\u2019s object";
-    };
-
-    // Set
-    var Set = function (items) {
-        this.items = [];
-        this.length = 0;
-        this.type = "set";
-        if (items) {
-            for (var i = 0, ii = items.length; i < ii; i++) {
-                if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) {
-                    this[this.items.length] = this.items[this.items.length] = items[i];
-                    this.length++;
-                }
-            }
-        }
-    },
-    setproto = Set.prototype;
-    /*\
-     * Set.push
-     [ method ]
-     **
-     * Adds each argument to the current set.
-     = (object) original element
-    \*/
-    setproto.push = function () {
-        var item,
-            len;
-        for (var i = 0, ii = arguments.length; i < ii; i++) {
-            item = arguments[i];
-            if (item && (item.constructor == elproto.constructor || item.constructor == Set)) {
-                len = this.items.length;
-                this[len] = this.items[len] = item;
-                this.length++;
-            }
-        }
-        return this;
-    };
-    /*\
-     * Set.pop
-     [ method ]
-     **
-     * Removes last element and returns it.
-     = (object) element
-    \*/
-    setproto.pop = function () {
-        this.length && delete this[this.length--];
-        return this.items.pop();
-    };
-    /*\
-     * Set.forEach
-     [ method ]
-     **
-     * Executes given function for each element in the set.
-     *
-     * If function returns `false` it will stop loop running.
-     **
-     > Parameters
-     **
-     - callback (function) function to run
-     - thisArg (object) context object for the callback
-     = (object) Set object
-    \*/
-    setproto.forEach = function (callback, thisArg) {
-        for (var i = 0, ii = this.items.length; i < ii; i++) {
-            if (callback.call(thisArg, this.items[i], i) === false) {
-                return this;
-            }
-        }
-        return this;
-    };
-    for (var method in elproto) if (elproto[has](method)) {
-        setproto[method] = (function (methodname) {
-            return function () {
-                var arg = arguments;
-                return this.forEach(function (el) {
-                    el[methodname][apply](el, arg);
-                });
-            };
-        })(method);
-    }
-    setproto.attr = function (name, value) {
-        if (name && R.is(name, array) && R.is(name[0], "object")) {
-            for (var j = 0, jj = name.length; j < jj; j++) {
-                this.items[j].attr(name[j]);
-            }
-        } else {
-            for (var i = 0, ii = this.items.length; i < ii; i++) {
-                this.items[i].attr(name, value);
-            }
-        }
-        return this;
-    };
-    /*\
-     * Set.clear
-     [ method ]
-     **
-     * Removes all elements from the set
-    \*/
-    setproto.clear = function () {
-        while (this.length) {
-            this.pop();
-        }
-    };
-    /*\
-     * Set.splice
-     [ method ]
-     **
-     * Removes given element from the set
-     **
-     > Parameters
-     **
-     - index (number) position of the deletion
-     - count (number) number of element to remove
-     - insertion… (object) #optional elements to insert
-     = (object) set elements that were deleted
-    \*/
-    setproto.splice = function (index, count, insertion) {
-        index = index < 0 ? mmax(this.length + index, 0) : index;
-        count = mmax(0, mmin(this.length - index, count));
-        var tail = [],
-            todel = [],
-            args = [],
-            i;
-        for (i = 2; i < arguments.length; i++) {
-            args.push(arguments[i]);
-        }
-        for (i = 0; i < count; i++) {
-            todel.push(this[index + i]);
-        }
-        for (; i < this.length - index; i++) {
-            tail.push(this[index + i]);
-        }
-        var arglen = args.length;
-        for (i = 0; i < arglen + tail.length; i++) {
-            this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen];
-        }
-        i = this.items.length = this.length -= count - arglen;
-        while (this[i]) {
-            delete this[i++];
-        }
-        return new Set(todel);
-    };
-    /*\
-     * Set.exclude
-     [ method ]
-     **
-     * Removes given element from the set
-     **
-     > Parameters
-     **
-     - element (object) element to remove
-     = (boolean) `true` if object was found & removed from the set
-    \*/
-    setproto.exclude = function (el) {
-        for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) {
-            this.splice(i, 1);
-            return true;
-        }
-    };
-    setproto.animate = function (params, ms, easing, callback) {
-        (R.is(easing, "function") || !easing) && (callback = easing || null);
-        var len = this.items.length,
-            i = len,
-            item,
-            set = this,
-            collector;
-        if (!len) {
-            return this;
-        }
-        callback && (collector = function () {
-            !--len && callback.call(set);
-        });
-        easing = R.is(easing, string) ? easing : collector;
-        var anim = R.animation(params, ms, easing, collector);
-        item = this.items[--i].animate(anim);
-        while (i--) {
-            this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim);
-            (this.items[i] && !this.items[i].removed) || len--;
-        }
-        return this;
-    };
-    setproto.insertAfter = function (el) {
-        var i = this.items.length;
-        while (i--) {
-            this.items[i].insertAfter(el);
-        }
-        return this;
-    };
-    setproto.getBBox = function () {
-        var x = [],
-            y = [],
-            x2 = [],
-            y2 = [];
-        for (var i = this.items.length; i--;) if (!this.items[i].removed) {
-            var box = this.items[i].getBBox();
-            x.push(box.x);
-            y.push(box.y);
-            x2.push(box.x + box.width);
-            y2.push(box.y + box.height);
-        }
-        x = mmin[apply](0, x);
-        y = mmin[apply](0, y);
-        x2 = mmax[apply](0, x2);
-        y2 = mmax[apply](0, y2);
-        return {
-            x: x,
-            y: y,
-            x2: x2,
-            y2: y2,
-            width: x2 - x,
-            height: y2 - y
-        };
-    };
-    setproto.clone = function (s) {
-        s = this.paper.set();
-        for (var i = 0, ii = this.items.length; i < ii; i++) {
-            s.push(this.items[i].clone());
-        }
-        return s;
-    };
-    setproto.toString = function () {
-        return "Rapha\xebl\u2018s set";
-    };
-
-    setproto.glow = function(glowConfig) {
-        var ret = this.paper.set();
-        this.forEach(function(shape, index){
-            var g = shape.glow(glowConfig);
-            if(g != null){
-                g.forEach(function(shape2, index2){
-                    ret.push(shape2);
-                });
-            }
-        });
-        return ret;
-    };
-
-
-    /*\
-     * Set.isPointInside
-     [ method ]
-     **
-     * Determine if given point is inside this set’s elements
-     **
-     > Parameters
-     **
-     - x (number) x coordinate of the point
-     - y (number) y coordinate of the point
-     = (boolean) `true` if point is inside any of the set's elements
-     \*/
-    setproto.isPointInside = function (x, y) {
-        var isPointInside = false;
-        this.forEach(function (el) {
-            if (el.isPointInside(x, y)) {
-                isPointInside = true;
-                return false; // stop loop
-            }
-        });
-        return isPointInside;
-    };
-
-    /*\
-     * Raphael.registerFont
-     [ method ]
-     **
-     * Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file.
-     * Returns original parameter, so it could be used with chaining.
-     # <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a>
-     **
-     > Parameters
-     **
-     - font (object) the font to register
-     = (object) the font you passed in
-     > Usage
-     | Cufon.registerFont(Raphael.registerFont({…}));
-    \*/
-    R.registerFont = function (font) {
-        if (!font.face) {
-            return font;
-        }
-        this.fonts = this.fonts || {};
-        var fontcopy = {
-                w: font.w,
-                face: {},
-                glyphs: {}
-            },
-            family = font.face["font-family"];
-        for (var prop in font.face) if (font.face[has](prop)) {
-            fontcopy.face[prop] = font.face[prop];
-        }
-        if (this.fonts[family]) {
-            this.fonts[family].push(fontcopy);
-        } else {
-            this.fonts[family] = [fontcopy];
-        }
-        if (!font.svg) {
-            fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
-            for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
-                var path = font.glyphs[glyph];
-                fontcopy.glyphs[glyph] = {
-                    w: path.w,
-                    k: {},
-                    d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) {
-                            return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
-                        }) + "z"
-                };
-                if (path.k) {
-                    for (var k in path.k) if (path[has](k)) {
-                        fontcopy.glyphs[glyph].k[k] = path.k[k];
-                    }
-                }
-            }
-        }
-        return font;
-    };
-    /*\
-     * Paper.getFont
-     [ method ]
-     **
-     * Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”.
-     **
-     > Parameters
-     **
-     - family (string) font family name or any word from it
-     - weight (string) #optional font weight
-     - style (string) #optional font style
-     - stretch (string) #optional font stretch
-     = (object) the font object
-     > Usage
-     | paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
-    \*/
-    paperproto.getFont = function (family, weight, style, stretch) {
-        stretch = stretch || "normal";
-        style = style || "normal";
-        weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
-        if (!R.fonts) {
-            return;
-        }
-        var font = R.fonts[family];
-        if (!font) {
-            var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
-            for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
-                if (name.test(fontName)) {
-                    font = R.fonts[fontName];
-                    break;
-                }
-            }
-        }
-        var thefont;
-        if (font) {
-            for (var i = 0, ii = font.length; i < ii; i++) {
-                thefont = font[i];
-                if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
-                    break;
-                }
-            }
-        }
-        return thefont;
-    };
-    /*\
-     * Paper.print
-     [ method ]
-     **
-     * Creates path that represent given text written using given font at given position with given size.
-     * Result of the method is path element that contains whole text as a separate path.
-     **
-     > Parameters
-     **
-     - x (number) x position of the text
-     - y (number) y position of the text
-     - string (string) text to print
-     - font (object) font object, see @Paper.getFont
-     - size (number) #optional size of the font, default is `16`
-     - origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"`
-     - letter_spacing (number) #optional number in range `-1..1`, default is `0`
-     - line_spacing (number) #optional number in range `1..3`, default is `1`
-     = (object) resulting path element, which consist of all letters
-     > Usage
-     | var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"});
-    \*/
-    paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) {
-        origin = origin || "middle"; // baseline|middle
-        letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1);
-        line_spacing = mmax(mmin(line_spacing || 1, 3), 1);
-        var letters = Str(string)[split](E),
-            shift = 0,
-            notfirst = 0,
-            path = E,
-            scale;
-        R.is(font, "string") && (font = this.getFont(font));
-        if (font) {
-            scale = (size || 16) / font.face["units-per-em"];
-            var bb = font.face.bbox[split](separator),
-                top = +bb[0],
-                lineHeight = bb[3] - bb[1],
-                shifty = 0,
-                height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2);
-            for (var i = 0, ii = letters.length; i < ii; i++) {
-                if (letters[i] == "\n") {
-                    shift = 0;
-                    curr = 0;
-                    notfirst = 0;
-                    shifty += lineHeight * line_spacing;
-                } else {
-                    var prev = notfirst && font.glyphs[letters[i - 1]] || {},
-                        curr = font.glyphs[letters[i]];
-                    shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0;
-                    notfirst = 1;
-                }
-                if (curr && curr.d) {
-                    path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]);
-                }
-            }
-        }
-        return this.path(path).attr({
-            fill: "#000",
-            stroke: "none"
-        });
-    };
-
-    /*\
-     * Paper.add
-     [ method ]
-     **
-     * Imports elements in JSON array in format `{type: type, <attributes>}`
-     **
-     > Parameters
-     **
-     - json (array)
-     = (object) resulting set of imported elements
-     > Usage
-     | paper.add([
-     |     {
-     |         type: "circle",
-     |         cx: 10,
-     |         cy: 10,
-     |         r: 5
-     |     },
-     |     {
-     |         type: "rect",
-     |         x: 10,
-     |         y: 10,
-     |         width: 10,
-     |         height: 10,
-     |         fill: "#fc0"
-     |     }
-     | ]);
-    \*/
-    paperproto.add = function (json) {
-        if (R.is(json, "array")) {
-            var res = this.set(),
-                i = 0,
-                ii = json.length,
-                j;
-            for (; i < ii; i++) {
-                j = json[i] || {};
-                elements[has](j.type) && res.push(this[j.type]().attr(j));
-            }
-        }
-        return res;
-    };
-
-    /*\
-     * Raphael.format
-     [ method ]
-     **
-     * Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument.
-     **
-     > Parameters
-     **
-     - token (string) string to format
-     - … (string) rest of arguments will be treated as parameters for replacement
-     = (string) formated string
-     > Usage
-     | var x = 10,
-     |     y = 20,
-     |     width = 40,
-     |     height = 50;
-     | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
-     | paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width));
-    \*/
-    R.format = function (token, params) {
-        var args = R.is(params, array) ? [0][concat](params) : arguments;
-        token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) {
-            return args[++i] == null ? E : args[i];
-        }));
-        return token || E;
-    };
-    /*\
-     * Raphael.fullfill
-     [ method ]
-     **
-     * A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument.
-     **
-     > Parameters
-     **
-     - token (string) string to format
-     - json (object) object which properties will be used as a replacement
-     = (string) formated string
-     > Usage
-     | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
-     | paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", {
-     |     x: 10,
-     |     y: 20,
-     |     dim: {
-     |         width: 40,
-     |         height: 50,
-     |         "negative width": -40
-     |     }
-     | }));
-    \*/
-    R.fullfill = (function () {
-        var tokenRegex = /\{([^\}]+)\}/g,
-            objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
-            replacer = function (all, key, obj) {
-                var res = obj;
-                key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) {
-                    name = name || quotedName;
-                    if (res) {
-                        if (name in res) {
-                            res = res[name];
-                        }
-                        typeof res == "function" && isFunc && (res = res());
-                    }
-                });
-                res = (res == null || res == obj ? all : res) + "";
-                return res;
-            };
-        return function (str, obj) {
-            return String(str).replace(tokenRegex, function (all, key) {
-                return replacer(all, key, obj);
-            });
-        };
-    })();
-    /*\
-     * Raphael.ninja
-     [ method ]
-     **
-     * If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method.
-     * Beware, that in this case plugins could stop working, because they are depending on global variable existance.
-     **
-     = (object) Raphael object
-     > Usage
-     | (function (local_raphael) {
-     |     var paper = local_raphael(10, 10, 320, 200);
-     |     …
-     | })(Raphael.ninja());
-    \*/
-    R.ninja = function () {
-        oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael;
-        return R;
-    };
-    /*\
-     * Raphael.st
-     [ property (object) ]
-     **
-     * You can add your own method to elements and sets. It is wise to add a set method for each element method
-     * you added, so you will be able to call the same method on sets too.
-     **
-     * See also @Raphael.el.
-     > Usage
-     | Raphael.el.red = function () {
-     |     this.attr({fill: "#f00"});
-     | };
-     | Raphael.st.red = function () {
-     |     this.forEach(function (el) {
-     |         el.red();
-     |     });
-     | };
-     | // then use it
-     | paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red();
-    \*/
-    R.st = setproto;
-
-    eve.on("raphael.DOMload", function () {
-        loaded = true;
-    });
-
-    // Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
-    (function (doc, loaded, f) {
-        if (doc.readyState == null && doc.addEventListener){
-            doc.addEventListener(loaded, f = function () {
-                doc.removeEventListener(loaded, f, false);
-                doc.readyState = "complete";
-            }, false);
-            doc.readyState = "loading";
-        }
-        function isLoaded() {
-            (/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload");
-        }
-        isLoaded();
-    })(document, "DOMContentLoaded");
-
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël - JavaScript Vector Library                                 │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ SVG Module                                                          │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com)   │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com)             │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function(){
-    if (!R.svg) {
-        return;
-    }
-    var has = "hasOwnProperty",
-        Str = String,
-        toFloat = parseFloat,
-        toInt = parseInt,
-        math = Math,
-        mmax = math.max,
-        abs = math.abs,
-        pow = math.pow,
-        separator = /[, ]+/,
-        eve = R.eve,
-        E = "",
-        S = " ";
-    var xlink = "http://www.w3.org/1999/xlink",
-        markers = {
-            block: "M5,0 0,2.5 5,5z",
-            classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z",
-            diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z",
-            open: "M6,1 1,3.5 6,6",
-            oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z"
-        },
-        markerCounter = {};
-    R.toString = function () {
-        return  "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
-    };
-    var $ = function (el, attr) {
-        if (attr) {
-            if (typeof el == "string") {
-                el = $(el);
-            }
-            for (var key in attr) if (attr[has](key)) {
-                if (key.substring(0, 6) == "xlink:") {
-                    el.setAttributeNS(xlink, key.substring(6), Str(attr[key]));
-                } else {
-                    el.setAttribute(key, Str(attr[key]));
-                }
-            }
-        } else {
-            el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el);
-            el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
-        }
-        return el;
-    },
-    addGradientFill = function (element, gradient) {
-        var type = "linear",
-            id = element.id + gradient,
-            fx = .5, fy = .5,
-            o = element.node,
-            SVG = element.paper,
-            s = o.style,
-            el = R._g.doc.getElementById(id);
-        if (!el) {
-            gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) {
-                type = "radial";
-                if (_fx && _fy) {
-                    fx = toFloat(_fx);
-                    fy = toFloat(_fy);
-                    var dir = ((fy > .5) * 2 - 1);
-                    pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
-                        (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
-                        fy != .5 &&
-                        (fy = fy.toFixed(5) - 1e-5 * dir);
-                }
-                return E;
-            });
-            gradient = gradient.split(/\s*\-\s*/);
-            if (type == "linear") {
-                var angle = gradient.shift();
-                angle = -toFloat(angle);
-                if (isNaN(angle)) {
-                    return null;
-                }
-                var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))],
-                    max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1);
-                vector[2] *= max;
-                vector[3] *= max;
-                if (vector[2] < 0) {
-                    vector[0] = -vector[2];
-                    vector[2] = 0;
-                }
-                if (vector[3] < 0) {
-                    vector[1] = -vector[3];
-                    vector[3] = 0;
-                }
-            }
-            var dots = R._parseDots(gradient);
-            if (!dots) {
-                return null;
-            }
-            id = id.replace(/[\(\)\s,\xb0#]/g, "_");
-
-            if (element.gradient && id != element.gradient.id) {
-                SVG.defs.removeChild(element.gradient);
-                delete element.gradient;
-            }
-
-            if (!element.gradient) {
-                el = $(type + "Gradient", {id: id});
-                element.gradient = el;
-                $(el, type == "radial" ? {
-                    fx: fx,
-                    fy: fy
-                } : {
-                    x1: vector[0],
-                    y1: vector[1],
-                    x2: vector[2],
-                    y2: vector[3],
-                    gradientTransform: element.matrix.invert()
-                });
-                SVG.defs.appendChild(el);
-                for (var i = 0, ii = dots.length; i < ii; i++) {
-                    el.appendChild($("stop", {
-                        offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%",
-                        "stop-color": dots[i].color || "#fff"
-                    }));
-                }
-            }
-        }
-        $(o, {
-            fill: "url('" + document.location + "#" + id + "')",
-            opacity: 1,
-            "fill-opacity": 1
-        });
-        s.fill = E;
-        s.opacity = 1;
-        s.fillOpacity = 1;
-        return 1;
-    },
-    updatePosition = function (o) {
-        var bbox = o.getBBox(1);
-        $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
-    },
-    addArrow = function (o, value, isEnd) {
-        if (o.type == "path") {
-            var values = Str(value).toLowerCase().split("-"),
-                p = o.paper,
-                se = isEnd ? "end" : "start",
-                node = o.node,
-                attrs = o.attrs,
-                stroke = attrs["stroke-width"],
-                i = values.length,
-                type = "classic",
-                from,
-                to,
-                dx,
-                refX,
-                attr,
-                w = 3,
-                h = 3,
-                t = 5;
-            while (i--) {
-                switch (values[i]) {
-                    case "block":
-                    case "classic":
-                    case "oval":
-                    case "diamond":
-                    case "open":
-                    case "none":
-                        type = values[i];
-                        break;
-                    case "wide": h = 5; break;
-                    case "narrow": h = 2; break;
-                    case "long": w = 5; break;
-                    case "short": w = 2; break;
-                }
-            }
-            if (type == "open") {
-                w += 2;
-                h += 2;
-                t += 2;
-                dx = 1;
-                refX = isEnd ? 4 : 1;
-                attr = {
-                    fill: "none",
-                    stroke: attrs.stroke
-                };
-            } else {
-                refX = dx = w / 2;
-                attr = {
-                    fill: attrs.stroke,
-                    stroke: "none"
-                };
-            }
-            if (o._.arrows) {
-                if (isEnd) {
-                    o._.arrows.endPath && markerCounter[o._.arrows.endPath]--;
-                    o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--;
-                } else {
-                    o._.arrows.startPath && markerCounter[o._.arrows.startPath]--;
-                    o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--;
-                }
-            } else {
-                o._.arrows = {};
-            }
-            if (type != "none") {
-                var pathId = "raphael-marker-" + type,
-                    markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id;
-                if (!R._g.doc.getElementById(pathId)) {
-                    p.defs.appendChild($($("path"), {
-                        "stroke-linecap": "round",
-                        d: markers[type],
-                        id: pathId
-                    }));
-                    markerCounter[pathId] = 1;
-                } else {
-                    markerCounter[pathId]++;
-                }
-                var marker = R._g.doc.getElementById(markerId),
-                    use;
-                if (!marker) {
-                    marker = $($("marker"), {
-                        id: markerId,
-                        markerHeight: h,
-                        markerWidth: w,
-                        orient: "auto",
-                        refX: refX,
-                        refY: h / 2
-                    });
-                    use = $($("use"), {
-                        "xlink:href": "#" + pathId,
-                        transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")",
-                        "stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4)
-                    });
-                    marker.appendChild(use);
-                    p.defs.appendChild(marker);
-                    markerCounter[markerId] = 1;
-                } else {
-                    markerCounter[markerId]++;
-                    use = marker.getElementsByTagName("use")[0];
-                }
-                $(use, attr);
-                var delta = dx * (type != "diamond" && type != "oval");
-                if (isEnd) {
-                    from = o._.arrows.startdx * stroke || 0;
-                    to = R.getTotalLength(attrs.path) - delta * stroke;
-                } else {
-                    from = delta * stroke;
-                    to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
-                }
-                attr = {};
-                attr["marker-" + se] = "url(#" + markerId + ")";
-                if (to || from) {
-                    attr.d = R.getSubpath(attrs.path, from, to);
-                }
-                $(node, attr);
-                o._.arrows[se + "Path"] = pathId;
-                o._.arrows[se + "Marker"] = markerId;
-                o._.arrows[se + "dx"] = delta;
-                o._.arrows[se + "Type"] = type;
-                o._.arrows[se + "String"] = value;
-            } else {
-                if (isEnd) {
-                    from = o._.arrows.startdx * stroke || 0;
-                    to = R.getTotalLength(attrs.path) - from;
-                } else {
-                    from = 0;
-                    to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
-                }
-                o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)});
-                delete o._.arrows[se + "Path"];
-                delete o._.arrows[se + "Marker"];
-                delete o._.arrows[se + "dx"];
-                delete o._.arrows[se + "Type"];
-                delete o._.arrows[se + "String"];
-            }
-            for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) {
-                var item = R._g.doc.getElementById(attr);
-                item && item.parentNode.removeChild(item);
-            }
-        }
-    },
-    dasharray = {
-        "": [0],
-        "none": [0],
-        "-": [3, 1],
-        ".": [1, 1],
-        "-.": [3, 1, 1, 1],
-        "-..": [3, 1, 1, 1, 1, 1],
-        ". ": [1, 3],
-        "- ": [4, 3],
-        "--": [8, 3],
-        "- .": [4, 3, 1, 3],
-        "--.": [8, 3, 1, 3],
-        "--..": [8, 3, 1, 3, 1, 3]
-    },
-    addDashes = function (o, value, params) {
-        value = dasharray[Str(value).toLowerCase()];
-        if (value) {
-            var width = o.attrs["stroke-width"] || "1",
-                butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
-                dashes = [],
-                i = value.length;
-            while (i--) {
-                dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
-            }
-            $(o.node, {"stroke-dasharray": dashes.join(",")});
-        }
-    },
-    setFillAndStroke = function (o, params) {
-        var node = o.node,
-            attrs = o.attrs,
-            vis = node.style.visibility;
-        node.style.visibility = "hidden";
-        for (var att in params) {
-            if (params[has](att)) {
-                if (!R._availableAttrs[has](att)) {
-                    continue;
-                }
-                var value = params[att];
-                attrs[att] = value;
-                switch (att) {
-                    case "blur":
-                        o.blur(value);
-                        break;
-                    case "title":
-                        var title = node.getElementsByTagName("title");
-
-                        // Use the existing <title>.
-                        if (title.length && (title = title[0])) {
-                          title.firstChild.nodeValue = value;
-                        } else {
-                          title = $("title");
-                          var val = R._g.doc.createTextNode(value);
-                          title.appendChild(val);
-                          node.appendChild(title);
-                        }
-                        break;
-                    case "href":
-                    case "target":
-                        var pn = node.parentNode;
-                        if (pn.tagName.toLowerCase() != "a") {
-                            var hl = $("a");
-                            pn.insertBefore(hl, node);
-                            hl.appendChild(node);
-                            pn = hl;
-                        }
-                        if (att == "target") {
-                            pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value);
-                        } else {
-                            pn.setAttributeNS(xlink, att, value);
-                        }
-                        break;
-                    case "cursor":
-                        node.style.cursor = value;
-                        break;
-                    case "transform":
-                        o.transform(value);
-                        break;
-                    case "arrow-start":
-                        addArrow(o, value);
-                        break;
-                    case "arrow-end":
-                        addArrow(o, value, 1);
-                        break;
-                    case "clip-rect":
-                        var rect = Str(value).split(separator);
-                        if (rect.length == 4) {
-                            o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
-                            var el = $("clipPath"),
-                                rc = $("rect");
-                            el.id = R.createUUID();
-                            $(rc, {
-                                x: rect[0],
-                                y: rect[1],
-                                width: rect[2],
-                                height: rect[3]
-                            });
-                            el.appendChild(rc);
-                            o.paper.defs.appendChild(el);
-                            $(node, {"clip-path": "url(#" + el.id + ")"});
-                            o.clip = rc;
-                        }
-                        if (!value) {
-                            var path = node.getAttribute("clip-path");
-                            if (path) {
-                                var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E));
-                                clip && clip.parentNode.removeChild(clip);
-                                $(node, {"clip-path": E});
-                                delete o.clip;
-                            }
-                        }
-                    break;
-                    case "path":
-                        if (o.type == "path") {
-                            $(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"});
-                            o._.dirty = 1;
-                            if (o._.arrows) {
-                                "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
-                                "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
-                            }
-                        }
-                        break;
-                    case "width":
-                        node.setAttribute(att, value);
-                        o._.dirty = 1;
-                        if (attrs.fx) {
-                            att = "x";
-                            value = attrs.x;
-                        } else {
-                            break;
-                        }
-                    case "x":
-                        if (attrs.fx) {
-                            value = -attrs.x - (attrs.width || 0);
-                        }
-                    case "rx":
-                        if (att == "rx" && o.type == "rect") {
-                            break;
-                        }
-                    case "cx":
-                        node.setAttribute(att, value);
-                        o.pattern && updatePosition(o);
-                        o._.dirty = 1;
-                        break;
-                    case "height":
-                        node.setAttribute(att, value);
-                        o._.dirty = 1;
-                        if (attrs.fy) {
-                            att = "y";
-                            value = attrs.y;
-                        } else {
-                            break;
-                        }
-                    case "y":
-                        if (attrs.fy) {
-                            value = -attrs.y - (attrs.height || 0);
-                        }
-                    case "ry":
-                        if (att == "ry" && o.type == "rect") {
-                            break;
-                        }
-                    case "cy":
-                        node.setAttribute(att, value);
-                        o.pattern && updatePosition(o);
-                        o._.dirty = 1;
-                        break;
-                    case "r":
-                        if (o.type == "rect") {
-                            $(node, {rx: value, ry: value});
-                        } else {
-                            node.setAttribute(att, value);
-                        }
-                        o._.dirty = 1;
-                        break;
-                    case "src":
-                        if (o.type == "image") {
-                            node.setAttributeNS(xlink, "href", value);
-                        }
-                        break;
-                    case "stroke-width":
-                        if (o._.sx != 1 || o._.sy != 1) {
-                            value /= mmax(abs(o._.sx), abs(o._.sy)) || 1;
-                        }
-                        node.setAttribute(att, value);
-                        if (attrs["stroke-dasharray"]) {
-                            addDashes(o, attrs["stroke-dasharray"], params);
-                        }
-                        if (o._.arrows) {
-                            "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
-                            "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
-                        }
-                        break;
-                    case "stroke-dasharray":
-                        addDashes(o, value, params);
-                        break;
-                    case "fill":
-                        var isURL = Str(value).match(R._ISURL);
-                        if (isURL) {
-                            el = $("pattern");
-                            var ig = $("image");
-                            el.id = R.createUUID();
-                            $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
-                            $(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
-                            el.appendChild(ig);
-
-                            (function (el) {
-                                R._preload(isURL[1], function () {
-                                    var w = this.offsetWidth,
-                                        h = this.offsetHeight;
-                                    $(el, {width: w, height: h});
-                                    $(ig, {width: w, height: h});
-                                    o.paper.safari();
-                                });
-                            })(el);
-                            o.paper.defs.appendChild(el);
-                            $(node, {fill: "url(#" + el.id + ")"});
-                            o.pattern = el;
-                            o.pattern && updatePosition(o);
-                            break;
-                        }
-                        var clr = R.getRGB(value);
-                        if (!clr.error) {
-                            delete params.gradient;
-                            delete attrs.gradient;
-                            !R.is(attrs.opacity, "undefined") &&
-                                R.is(params.opacity, "undefined") &&
-                                $(node, {opacity: attrs.opacity});
-                            !R.is(attrs["fill-opacity"], "undefined") &&
-                                R.is(params["fill-opacity"], "undefined") &&
-                                $(node, {"fill-opacity": attrs["fill-opacity"]});
-                        } else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) {
-                            if ("opacity" in attrs || "fill-opacity" in attrs) {
-                                var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
-                                if (gradient) {
-                                    var stops = gradient.getElementsByTagName("stop");
-                                    $(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)});
-                                }
-                            }
-                            attrs.gradient = value;
-                            attrs.fill = "none";
-                            break;
-                        }
-                        clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
-                    case "stroke":
-                        clr = R.getRGB(value);
-                        node.setAttribute(att, clr.hex);
-                        att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
-                        if (att == "stroke" && o._.arrows) {
-                            "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
-                            "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
-                        }
-                        break;
-                    case "gradient":
-                        (o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value);
-                        break;
-                    case "opacity":
-                        if (attrs.gradient && !attrs[has]("stroke-opacity")) {
-                            $(node, {"stroke-opacity": value > 1 ? value / 100 : value});
-                        }
-                        // fall
-                    case "fill-opacity":
-                        if (attrs.gradient) {
-                            gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
-                            if (gradient) {
-                                stops = gradient.getElementsByTagName("stop");
-                                $(stops[stops.length - 1], {"stop-opacity": value});
-                            }
-                            break;
-                        }
-                    default:
-                        att == "font-size" && (value = toInt(value, 10) + "px");
-                        var cssrule = att.replace(/(\-.)/g, function (w) {
-                            return w.substring(1).toUpperCase();
-                        });
-                        node.style[cssrule] = value;
-                        o._.dirty = 1;
-                        node.setAttribute(att, value);
-                        break;
-                }
-            }
-        }
-
-        tuneText(o, params);
-        node.style.visibility = vis;
-    },
-    leading = 1.2,
-    tuneText = function (el, params) {
-        if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
-            return;
-        }
-        var a = el.attrs,
-            node = el.node,
-            fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
-
-        if (params[has]("text")) {
-            a.text = params.text;
-            while (node.firstChild) {
-                node.removeChild(node.firstChild);
-            }
-            var texts = Str(params.text).split("\n"),
-                tspans = [],
-                tspan;
-            for (var i = 0, ii = texts.length; i < ii; i++) {
-                tspan = $("tspan");
-                i && $(tspan, {dy: fontSize * leading, x: a.x});
-                tspan.appendChild(R._g.doc.createTextNode(texts[i]));
-                node.appendChild(tspan);
-                tspans[i] = tspan;
-            }
-        } else {
-            tspans = node.getElementsByTagName("tspan");
-            for (i = 0, ii = tspans.length; i < ii; i++) if (i) {
-                $(tspans[i], {dy: fontSize * leading, x: a.x});
-            } else {
-                $(tspans[0], {dy: 0});
-            }
-        }
-        $(node, {x: a.x, y: a.y});
-        el._.dirty = 1;
-        var bb = el._getBBox(),
-            dif = a.y - (bb.y + bb.height / 2);
-        dif && R.is(dif, "finite") && $(tspans[0], {dy: dif});
-    },
-    getRealNode = function (node) {
-        if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") {
-            return node.parentNode;
-        } else {
-            return node;
-        }
-    },
-    Element = function (node, svg) {
-        var X = 0,
-            Y = 0;
-        /*\
-         * Element.node
-         [ property (object) ]
-         **
-         * Gives you a reference to the DOM object, so you can assign event handlers or just mess around.
-         **
-         * Note: Don’t mess with it.
-         > Usage
-         | // draw a circle at coordinate 10,10 with radius of 10
-         | var c = paper.circle(10, 10, 10);
-         | c.node.onclick = function () {
-         |     c.attr("fill", "red");
-         | };
-        \*/
-        this[0] = this.node = node;
-        /*\
-         * Element.raphael
-         [ property (object) ]
-         **
-         * Internal reference to @Raphael object. In case it is not available.
-         > Usage
-         | Raphael.el.red = function () {
-         |     var hsb = this.paper.raphael.rgb2hsb(this.attr("fill"));
-         |     hsb.h = 1;
-         |     this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex});
-         | }
-        \*/
-        node.raphael = true;
-        /*\
-         * Element.id
-         [ property (number) ]
-         **
-         * Unique id of the element. Especially useful when you want to listen to events of the element,
-         * because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method.
-        \*/
-        this.id = R._oid++;
-        node.raphaelid = this.id;
-        this.matrix = R.matrix();
-        this.realPath = null;
-        /*\
-         * Element.paper
-         [ property (object) ]
-         **
-         * Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions.
-         > Usage
-         | Raphael.el.cross = function () {
-         |     this.attr({fill: "red"});
-         |     this.paper.path("M10,10L50,50M50,10L10,50")
-         |         .attr({stroke: "red"});
-         | }
-        \*/
-        this.paper = svg;
-        this.attrs = this.attrs || {};
-        this._ = {
-            transform: [],
-            sx: 1,
-            sy: 1,
-            deg: 0,
-            dx: 0,
-            dy: 0,
-            dirty: 1
-        };
-        !svg.bottom && (svg.bottom = this);
-        /*\
-         * Element.prev
-         [ property (object) ]
-         **
-         * Reference to the previous element in the hierarchy.
-        \*/
-        this.prev = svg.top;
-        svg.top && (svg.top.next = this);
-        svg.top = this;
-        /*\
-         * Element.next
-         [ property (object) ]
-         **
-         * Reference to the next element in the hierarchy.
-        \*/
-        this.next = null;
-    },
-    elproto = R.el;
-
-    Element.prototype = elproto;
-    elproto.constructor = Element;
-
-    R._engine.path = function (pathString, SVG) {
-        var el = $("path");
-        SVG.canvas && SVG.canvas.appendChild(el);
-        var p = new Element(el, SVG);
-        p.type = "path";
-        setFillAndStroke(p, {
-            fill: "none",
-            stroke: "#000",
-            path: pathString
-        });
-        return p;
-    };
-    /*\
-     * Element.rotate
-     [ method ]
-     **
-     * Deprecated! Use @Element.transform instead.
-     * Adds rotation by given angle around given point to the list of
-     * transformations of the element.
-     > Parameters
-     - deg (number) angle in degrees
-     - cx (number) #optional x coordinate of the centre of rotation
-     - cy (number) #optional y coordinate of the centre of rotation
-     * If cx & cy aren’t specified centre of the shape is used as a point of rotation.
-     = (object) @Element
-    \*/
-    elproto.rotate = function (deg, cx, cy) {
-        if (this.removed) {
-            return this;
-        }
-        deg = Str(deg).split(separator);
-        if (deg.length - 1) {
-            cx = toFloat(deg[1]);
-            cy = toFloat(deg[2]);
-        }
-        deg = toFloat(deg[0]);
-        (cy == null) && (cx = cy);
-        if (cx == null || cy == null) {
-            var bbox = this.getBBox(1);
-            cx = bbox.x + bbox.width / 2;
-            cy = bbox.y + bbox.height / 2;
-        }
-        this.transform(this._.transform.concat([["r", deg, cx, cy]]));
-        return this;
-    };
-    /*\
-     * Element.scale
-     [ method ]
-     **
-     * Deprecated! Use @Element.transform instead.
-     * Adds scale by given amount relative to given point to the list of
-     * transformations of the element.
-     > Parameters
-     - sx (number) horisontal scale amount
-     - sy (number) vertical scale amount
-     - cx (number) #optional x coordinate of the centre of scale
-     - cy (number) #optional y coordinate of the centre of scale
-     * If cx & cy aren’t specified centre of the shape is used instead.
-     = (object) @Element
-    \*/
-    elproto.scale = function (sx, sy, cx, cy) {
-        if (this.removed) {
-            return this;
-        }
-        sx = Str(sx).split(separator);
-        if (sx.length - 1) {
-            sy = toFloat(sx[1]);
-            cx = toFloat(sx[2]);
-            cy = toFloat(sx[3]);
-        }
-        sx = toFloat(sx[0]);
-        (sy == null) && (sy = sx);
-        (cy == null) && (cx = cy);
-        if (cx == null || cy == null) {
-            var bbox = this.getBBox(1);
-        }
-        cx = cx == null ? bbox.x + bbox.width / 2 : cx;
-        cy = cy == null ? bbox.y + bbox.height / 2 : cy;
-        this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
-        return this;
-    };
-    /*\
-     * Element.translate
-     [ method ]
-     **
-     * Deprecated! Use @Element.transform instead.
-     * Adds translation by given amount to the list of transformations of the element.
-     > Parameters
-     - dx (number) horisontal shift
-     - dy (number) vertical shift
-     = (object) @Element
-    \*/
-    elproto.translate = function (dx, dy) {
-        if (this.removed) {
-            return this;
-        }
-        dx = Str(dx).split(separator);
-        if (dx.length - 1) {
-            dy = toFloat(dx[1]);
-        }
-        dx = toFloat(dx[0]) || 0;
-        dy = +dy || 0;
-        this.transform(this._.transform.concat([["t", dx, dy]]));
-        return this;
-    };
-    /*\
-     * Element.transform
-     [ method ]
-     **
-     * Adds transformation to the element which is separate to other attributes,
-     * i.e. translation doesn’t change `x` or `y` of the rectange. The format
-     * of transformation string is similar to the path string syntax:
-     | "t100,100r30,100,100s2,2,100,100r45s1.5"
-     * Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for
-     * scale and `m` is for matrix.
-     *
-     * There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`.
-     *
-     * So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100;
-     * rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin
-     * coordinates as optional parameters, the default is the centre point of the element.
-     * Matrix accepts six parameters.
-     > Usage
-     | var el = paper.rect(10, 20, 300, 200);
-     | // translate 100, 100, rotate 45°, translate -100, 0
-     | el.transform("t100,100r45t-100,0");
-     | // if you want you can append or prepend transformations
-     | el.transform("...t50,50");
-     | el.transform("s2...");
-     | // or even wrap
-     | el.transform("t50,50...t-50-50");
-     | // to reset transformation call method with empty string
-     | el.transform("");
-     | // to get current value call it without parameters
-     | console.log(el.transform());
-     > Parameters
-     - tstr (string) #optional transformation string
-     * If tstr isn’t specified
-     = (string) current transformation string
-     * else
-     = (object) @Element
-    \*/
-    elproto.transform = function (tstr) {
-        var _ = this._;
-        if (tstr == null) {
-            return _.transform;
-        }
-        R._extractTransform(this, tstr);
-
-        this.clip && $(this.clip, {transform: this.matrix.invert()});
-        this.pattern && updatePosition(this);
-        this.node && $(this.node, {transform: this.matrix});
-
-        if (_.sx != 1 || _.sy != 1) {
-            var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1;
-            this.attr({"stroke-width": sw});
-        }
-
-        return this;
-    };
-    /*\
-     * Element.hide
-     [ method ]
-     **
-     * Makes element invisible. See @Element.show.
-     = (object) @Element
-    \*/
-    elproto.hide = function () {
-        !this.removed && this.paper.safari(this.node.style.display = "none");
-        return this;
-    };
-    /*\
-     * Element.show
-     [ method ]
-     **
-     * Makes element visible. See @Element.hide.
-     = (object) @Element
-    \*/
-    elproto.show = function () {
-        !this.removed && this.paper.safari(this.node.style.display = "");
-        return this;
-    };
-    /*\
-     * Element.remove
-     [ method ]
-     **
-     * Removes element from the paper.
-    \*/
-    elproto.remove = function () {
-        var node = getRealNode(this.node);
-        if (this.removed || !node.parentNode) {
-            return;
-        }
-        var paper = this.paper;
-        paper.__set__ && paper.__set__.exclude(this);
-        eve.unbind("raphael.*.*." + this.id);
-        if (this.gradient) {
-            paper.defs.removeChild(this.gradient);
-        }
-        R._tear(this, paper);
-
-        node.parentNode.removeChild(node);
-
-        // Remove custom data for element
-        this.removeData();
-
-        for (var i in this) {
-            this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
-        }
-        this.removed = true;
-    };
-    elproto._getBBox = function () {
-        if (this.node.style.display == "none") {
-            this.show();
-            var hide = true;
-        }
-        var canvasHidden = false,
-            containerStyle;
-        if (this.paper.canvas.parentElement) {
-          containerStyle = this.paper.canvas.parentElement.style;
-        } //IE10+ can't find parentElement
-        else if (this.paper.canvas.parentNode) {
-          containerStyle = this.paper.canvas.parentNode.style;
-        }
-
-        if(containerStyle && containerStyle.display == "none") {
-          canvasHidden = true;
-          containerStyle.display = "";
-        }
-        var bbox = {};
-        try {
-            bbox = this.node.getBBox();
-        } catch(e) {
-            // Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix
-            bbox = {
-                x: this.node.clientLeft,
-                y: this.node.clientTop,
-                width: this.node.clientWidth,
-                height: this.node.clientHeight
-            }
-        } finally {
-            bbox = bbox || {};
-            if(canvasHidden){
-              containerStyle.display = "none";
-            }
-        }
-        hide && this.hide();
-        return bbox;
-    };
-    /*\
-     * Element.attr
-     [ method ]
-     **
-     * Sets the attributes of the element.
-     > Parameters
-     - attrName (string) attribute’s name
-     - value (string) value
-     * or
-     - params (object) object of name/value pairs
-     * or
-     - attrName (string) attribute’s name
-     * or
-     - attrNames (array) in this case method returns array of current values for given attribute names
-     = (object) @Element if attrsName & value or params are passed in.
-     = (...) value of the attribute if only attrsName is passed in.
-     = (array) array of values of the attribute if attrsNames is passed in.
-     = (object) object of attributes if nothing is passed in.
-     > Possible parameters
-     # <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p>
-     o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`.
-     o clip-rect (string) comma or space separated values: x, y, width and height
-     o cursor (string) CSS type of the cursor
-     o cx (number) the x-axis coordinate of the center of the circle, or ellipse
-     o cy (number) the y-axis coordinate of the center of the circle, or ellipse
-     o fill (string) colour, gradient or image
-     o fill-opacity (number)
-     o font (string)
-     o font-family (string)
-     o font-size (number) font size in pixels
-     o font-weight (string)
-     o height (number)
-     o href (string) URL, if specified element behaves as hyperlink
-     o opacity (number)
-     o path (string) SVG path string format
-     o r (number) radius of the circle, ellipse or rounded corner on the rect
-     o rx (number) horisontal radius of the ellipse
-     o ry (number) vertical radius of the ellipse
-     o src (string) image URL, only works for @Element.image element
-     o stroke (string) stroke colour
-     o stroke-dasharray (string) [“”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”]
-     o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”]
-     o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”]
-     o stroke-miterlimit (number)
-     o stroke-opacity (number)
-     o stroke-width (number) stroke width in pixels, default is '1'
-     o target (string) used with href
-     o text (string) contents of the text element. Use `\n` for multiline text
-     o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`”
-     o title (string) will create tooltip with a given text
-     o transform (string) see @Element.transform
-     o width (number)
-     o x (number)
-     o y (number)
-     > Gradients
-     * Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90°
-     * gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black.
-     *
-     * radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” –
-     * gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point
-     * at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses.
-     > Path String
-     # <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p>
-     > Colour Parsing
-     # <ul>
-     #     <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
-     #     <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
-     #     <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
-     #     <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
-     #     <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
-     #     <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200,&nbsp;100,&nbsp;0, .5)</code>”)</li>
-     #     <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%,&nbsp;175%,&nbsp;0%, 50%)</code>”)</li>
-     #     <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
-     #     <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
-     #     <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li>
-     #     <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li>
-     #     <li>hsl(•••%, •••%, •••%) — same as above, but in %</li>
-     #     <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li>
-     #     <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg,&nbsp;1,&nbsp;.5)</code>” or, if you want to go fancy, “<code>hsl(240°,&nbsp;1,&nbsp;.5)</code>”</li>
-     # </ul>
-    \*/
-    elproto.attr = function (name, value) {
-        if (this.removed) {
-            return this;
-        }
-        if (name == null) {
-            var res = {};
-            for (var a in this.attrs) if (this.attrs[has](a)) {
-                res[a] = this.attrs[a];
-            }
-            res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
-            res.transform = this._.transform;
-            return res;
-        }
-        if (value == null && R.is(name, "string")) {
-            if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
-                return this.attrs.gradient;
-            }
-            if (name == "transform") {
-                return this._.transform;
-            }
-            var names = name.split(separator),
-                out = {};
-            for (var i = 0, ii = names.length; i < ii; i++) {
-                name = names[i];
-                if (name in this.attrs) {
-                    out[name] = this.attrs[name];
-                } else if (R.is(this.paper.customAttributes[name], "function")) {
-                    out[name] = this.paper.customAttributes[name].def;
-                } else {
-                    out[name] = R._availableAttrs[name];
-                }
-            }
-            return ii - 1 ? out : out[names[0]];
-        }
-        if (value == null && R.is(name, "array")) {
-            out = {};
-            for (i = 0, ii = name.length; i < ii; i++) {
-                out[name[i]] = this.attr(name[i]);
-            }
-            return out;
-        }
-        if (value != null) {
-            var params = {};
-            params[name] = value;
-        } else if (name != null && R.is(name, "object")) {
-            params = name;
-        }
-        for (var key in params) {
-            eve("raphael.attr." + key + "." + this.id, this, params[key]);
-        }
-        for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
-            var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
-            this.attrs[key] = params[key];
-            for (var subkey in par) if (par[has](subkey)) {
-                params[subkey] = par[subkey];
-            }
-        }
-        setFillAndStroke(this, params);
-        return this;
-    };
-    /*\
-     * Element.toFront
-     [ method ]
-     **
-     * Moves the element so it is the closest to the viewer’s eyes, on top of other elements.
-     = (object) @Element
-    \*/
-    elproto.toFront = function () {
-        if (this.removed) {
-            return this;
-        }
-        var node = getRealNode(this.node);
-        node.parentNode.appendChild(node);
-        var svg = this.paper;
-        svg.top != this && R._tofront(this, svg);
-        return this;
-    };
-    /*\
-     * Element.toBack
-     [ method ]
-     **
-     * Moves the element so it is the furthest from the viewer’s eyes, behind other elements.
-     = (object) @Element
-    \*/
-    elproto.toBack = function () {
-        if (this.removed) {
-            return this;
-        }
-        var node = getRealNode(this.node);
-        var parentNode = node.parentNode;
-        parentNode.insertBefore(node, parentNode.firstChild);
-        R._toback(this, this.paper);
-        var svg = this.paper;
-        return this;
-    };
-    /*\
-     * Element.insertAfter
-     [ method ]
-     **
-     * Inserts current object after the given one.
-     = (object) @Element
-    \*/
-    elproto.insertAfter = function (element) {
-        if (this.removed || !element) {
-            return this;
-        }
-
-        var node = getRealNode(this.node);
-        var afterNode = getRealNode(element.node || element[element.length - 1].node);
-        if (afterNode.nextSibling) {
-            afterNode.parentNode.insertBefore(node, afterNode.nextSibling);
-        } else {
-            afterNode.parentNode.appendChild(node);
-        }
-        R._insertafter(this, element, this.paper);
-        return this;
-    };
-    /*\
-     * Element.insertBefore
-     [ method ]
-     **
-     * Inserts current object before the given one.
-     = (object) @Element
-    \*/
-    elproto.insertBefore = function (element) {
-        if (this.removed || !element) {
-            return this;
-        }
-
-        var node = getRealNode(this.node);
-        var beforeNode = getRealNode(element.node || element[0].node);
-        beforeNode.parentNode.insertBefore(node, beforeNode);
-        R._insertbefore(this, element, this.paper);
-        return this;
-    };
-    elproto.blur = function (size) {
-        // Experimental. No Safari support. Use it on your own risk.
-        var t = this;
-        if (+size !== 0) {
-            var fltr = $("filter"),
-                blur = $("feGaussianBlur");
-            t.attrs.blur = size;
-            fltr.id = R.createUUID();
-            $(blur, {stdDeviation: +size || 1.5});
-            fltr.appendChild(blur);
-            t.paper.defs.appendChild(fltr);
-            t._blur = fltr;
-            $(t.node, {filter: "url(#" + fltr.id + ")"});
-        } else {
-            if (t._blur) {
-                t._blur.parentNode.removeChild(t._blur);
-                delete t._blur;
-                delete t.attrs.blur;
-            }
-            t.node.removeAttribute("filter");
-        }
-        return t;
-    };
-    R._engine.circle = function (svg, x, y, r) {
-        var el = $("circle");
-        svg.canvas && svg.canvas.appendChild(el);
-        var res = new Element(el, svg);
-        res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"};
-        res.type = "circle";
-        $(el, res.attrs);
-        return res;
-    };
-    R._engine.rect = function (svg, x, y, w, h, r) {
-        var el = $("rect");
-        svg.canvas && svg.canvas.appendChild(el);
-        var res = new Element(el, svg);
-        res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"};
-        res.type = "rect";
-        $(el, res.attrs);
-        return res;
-    };
-    R._engine.ellipse = function (svg, x, y, rx, ry) {
-        var el = $("ellipse");
-        svg.canvas && svg.canvas.appendChild(el);
-        var res = new Element(el, svg);
-        res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"};
-        res.type = "ellipse";
-        $(el, res.attrs);
-        return res;
-    };
-    R._engine.image = function (svg, src, x, y, w, h) {
-        var el = $("image");
-        $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"});
-        el.setAttributeNS(xlink, "href", src);
-        svg.canvas && svg.canvas.appendChild(el);
-        var res = new Element(el, svg);
-        res.attrs = {x: x, y: y, width: w, height: h, src: src};
-        res.type = "image";
-        return res;
-    };
-    R._engine.text = function (svg, x, y, text) {
-        var el = $("text");
-        svg.canvas && svg.canvas.appendChild(el);
-        var res = new Element(el, svg);
-        res.attrs = {
-            x: x,
-            y: y,
-            "text-anchor": "middle",
-            text: text,
-            "font-family": R._availableAttrs["font-family"],
-            "font-size": R._availableAttrs["font-size"],
-            stroke: "none",
-            fill: "#000"
-        };
-        res.type = "text";
-        setFillAndStroke(res, res.attrs);
-        return res;
-    };
-    R._engine.setSize = function (width, height) {
-        this.width = width || this.width;
-        this.height = height || this.height;
-        this.canvas.setAttribute("width", this.width);
-        this.canvas.setAttribute("height", this.height);
-        if (this._viewBox) {
-            this.setViewBox.apply(this, this._viewBox);
-        }
-        return this;
-    };
-    R._engine.create = function () {
-        var con = R._getContainer.apply(0, arguments),
-            container = con && con.container,
-            x = con.x,
-            y = con.y,
-            width = con.width,
-            height = con.height;
-        if (!container) {
-            throw new Error("SVG container not found.");
-        }
-        var cnvs = $("svg"),
-            css = "overflow:hidden;",
-            isFloating;
-        x = x || 0;
-        y = y || 0;
-        width = width || 512;
-        height = height || 342;
-        $(cnvs, {
-            height: height,
-            version: 1.1,
-            width: width,
-            xmlns: "http://www.w3.org/2000/svg",
-            "xmlns:xlink": "http://www.w3.org/1999/xlink"
-        });
-        if (container == 1) {
-            cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px";
-            R._g.doc.body.appendChild(cnvs);
-            isFloating = 1;
-        } else {
-            cnvs.style.cssText = css + "position:relative";
-            if (container.firstChild) {
-                container.insertBefore(cnvs, container.firstChild);
-            } else {
-                container.appendChild(cnvs);
-            }
-        }
-        container = new R._Paper;
-        container.width = width;
-        container.height = height;
-        container.canvas = cnvs;
-        container.clear();
-        container._left = container._top = 0;
-        isFloating && (container.renderfix = function () {});
-        container.renderfix();
-        return container;
-    };
-    R._engine.setViewBox = function (x, y, w, h, fit) {
-        eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
-        var paperSize = this.getSize(),
-            size = mmax(w / paperSize.width, h / paperSize.height),
-            top = this.top,
-            aspectRatio = fit ? "xMidYMid meet" : "xMinYMin",
-            vb,
-            sw;
-        if (x == null) {
-            if (this._vbSize) {
-                size = 1;
-            }
-            delete this._vbSize;
-            vb = "0 0 " + this.width + S + this.height;
-        } else {
-            this._vbSize = size;
-            vb = x + S + y + S + w + S + h;
-        }
-        $(this.canvas, {
-            viewBox: vb,
-            preserveAspectRatio: aspectRatio
-        });
-        while (size && top) {
-            sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1;
-            top.attr({"stroke-width": sw});
-            top._.dirty = 1;
-            top._.dirtyT = 1;
-            top = top.prev;
-        }
-        this._viewBox = [x, y, w, h, !!fit];
-        return this;
-    };
-    /*\
-     * Paper.renderfix
-     [ method ]
-     **
-     * Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant
-     * on other elements after reflow it could shift half pixel which cause for lines to lost their crispness.
-     * This method fixes the issue.
-     **
-       Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method.
-    \*/
-    R.prototype.renderfix = function () {
-        var cnvs = this.canvas,
-            s = cnvs.style,
-            pos;
-        try {
-            pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix();
-        } catch (e) {
-            pos = cnvs.createSVGMatrix();
-        }
-        var left = -pos.e % 1,
-            top = -pos.f % 1;
-        if (left || top) {
-            if (left) {
-                this._left = (this._left + left) % 1;
-                s.left = this._left + "px";
-            }
-            if (top) {
-                this._top = (this._top + top) % 1;
-                s.top = this._top + "px";
-            }
-        }
-    };
-    /*\
-     * Paper.clear
-     [ method ]
-     **
-     * Clears the paper, i.e. removes all the elements.
-    \*/
-    R.prototype.clear = function () {
-        R.eve("raphael.clear", this);
-        var c = this.canvas;
-        while (c.firstChild) {
-            c.removeChild(c.firstChild);
-        }
-        this.bottom = this.top = null;
-        (this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version));
-        c.appendChild(this.desc);
-        c.appendChild(this.defs = $("defs"));
-    };
-    /*\
-     * Paper.remove
-     [ method ]
-     **
-     * Removes the paper from the DOM.
-    \*/
-    R.prototype.remove = function () {
-        eve("raphael.remove", this);
-        this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas);
-        for (var i in this) {
-            this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
-        }
-    };
-    var setproto = R.st;
-    for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
-        setproto[method] = (function (methodname) {
-            return function () {
-                var arg = arguments;
-                return this.forEach(function (el) {
-                    el[methodname].apply(el, arg);
-                });
-            };
-        })(method);
-    }
-})();
-
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël - JavaScript Vector Library                                 │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ VML Module                                                          │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com)   │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com)             │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function(){
-    if (!R.vml) {
-        return;
-    }
-    var has = "hasOwnProperty",
-        Str = String,
-        toFloat = parseFloat,
-        math = Math,
-        round = math.round,
-        mmax = math.max,
-        mmin = math.min,
-        abs = math.abs,
-        fillString = "fill",
-        separator = /[, ]+/,
-        eve = R.eve,
-        ms = " progid:DXImageTransform.Microsoft",
-        S = " ",
-        E = "",
-        map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
-        bites = /([clmz]),?([^clmz]*)/gi,
-        blurregexp = / progid:\S+Blur\([^\)]+\)/g,
-        val = /-?[^,\s-]+/g,
-        cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)",
-        zoom = 21600,
-        pathTypes = {path: 1, rect: 1, image: 1},
-        ovalTypes = {circle: 1, ellipse: 1},
-        path2vml = function (path) {
-            var total =  /[ahqstv]/ig,
-                command = R._pathToAbsolute;
-            Str(path).match(total) && (command = R._path2curve);
-            total = /[clmz]/g;
-            if (command == R._pathToAbsolute && !Str(path).match(total)) {
-                var res = Str(path).replace(bites, function (all, command, args) {
-                    var vals = [],
-                        isMove = command.toLowerCase() == "m",
-                        res = map[command];
-                    args.replace(val, function (value) {
-                        if (isMove && vals.length == 2) {
-                            res += vals + map[command == "m" ? "l" : "L"];
-                            vals = [];
-                        }
-                        vals.push(round(value * zoom));
-                    });
-                    return res + vals;
-                });
-                return res;
-            }
-            var pa = command(path), p, r;
-            res = [];
-            for (var i = 0, ii = pa.length; i < ii; i++) {
-                p = pa[i];
-                r = pa[i][0].toLowerCase();
-                r == "z" && (r = "x");
-                for (var j = 1, jj = p.length; j < jj; j++) {
-                    r += round(p[j] * zoom) + (j != jj - 1 ? "," : E);
-                }
-                res.push(r);
-            }
-            return res.join(S);
-        },
-        compensation = function (deg, dx, dy) {
-            var m = R.matrix();
-            m.rotate(-deg, .5, .5);
-            return {
-                dx: m.x(dx, dy),
-                dy: m.y(dx, dy)
-            };
-        },
-        setCoords = function (p, sx, sy, dx, dy, deg) {
-            var _ = p._,
-                m = p.matrix,
-                fillpos = _.fillpos,
-                o = p.node,
-                s = o.style,
-                y = 1,
-                flip = "",
-                dxdy,
-                kx = zoom / sx,
-                ky = zoom / sy;
-            s.visibility = "hidden";
-            if (!sx || !sy) {
-                return;
-            }
-            o.coordsize = abs(kx) + S + abs(ky);
-            s.rotation = deg * (sx * sy < 0 ? -1 : 1);
-            if (deg) {
-                var c = compensation(deg, dx, dy);
-                dx = c.dx;
-                dy = c.dy;
-            }
-            sx < 0 && (flip += "x");
-            sy < 0 && (flip += " y") && (y = -1);
-            s.flip = flip;
-            o.coordorigin = (dx * -kx) + S + (dy * -ky);
-            if (fillpos || _.fillsize) {
-                var fill = o.getElementsByTagName(fillString);
-                fill = fill && fill[0];
-                o.removeChild(fill);
-                if (fillpos) {
-                    c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1]));
-                    fill.position = c.dx * y + S + c.dy * y;
-                }
-                if (_.fillsize) {
-                    fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy);
-                }
-                o.appendChild(fill);
-            }
-            s.visibility = "visible";
-        };
-    R.toString = function () {
-        return  "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version;
-    };
-    var addArrow = function (o, value, isEnd) {
-        var values = Str(value).toLowerCase().split("-"),
-            se = isEnd ? "end" : "start",
-            i = values.length,
-            type = "classic",
-            w = "medium",
-            h = "medium";
-        while (i--) {
-            switch (values[i]) {
-                case "block":
-                case "classic":
-                case "oval":
-                case "diamond":
-                case "open":
-                case "none":
-                    type = values[i];
-                    break;
-                case "wide":
-                case "narrow": h = values[i]; break;
-                case "long":
-                case "short": w = values[i]; break;
-            }
-        }
-        var stroke = o.node.getElementsByTagName("stroke")[0];
-        stroke[se + "arrow"] = type;
-        stroke[se + "arrowlength"] = w;
-        stroke[se + "arrowwidth"] = h;
-    },
-    setFillAndStroke = function (o, params) {
-        // o.paper.canvas.style.display = "none";
-        o.attrs = o.attrs || {};
-        var node = o.node,
-            a = o.attrs,
-            s = node.style,
-            xy,
-            newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r),
-            isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry),
-            res = o;
-
-
-        for (var par in params) if (params[has](par)) {
-            a[par] = params[par];
-        }
-        if (newpath) {
-            a.path = R._getPath[o.type](o);
-            o._.dirty = 1;
-        }
-        params.href && (node.href = params.href);
-        params.title && (node.title = params.title);
-        params.target && (node.target = params.target);
-        params.cursor && (s.cursor = params.cursor);
-        "blur" in params && o.blur(params.blur);
-        if (params.path && o.type == "path" || newpath) {
-            node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path);
-            o._.dirty = 1;
-            if (o.type == "image") {
-                o._.fillpos = [a.x, a.y];
-                o._.fillsize = [a.width, a.height];
-                setCoords(o, 1, 1, 0, 0, 0);
-            }
-        }
-        "transform" in params && o.transform(params.transform);
-        if (isOval) {
-            var cx = +a.cx,
-                cy = +a.cy,
-                rx = +a.rx || +a.r || 0,
-                ry = +a.ry || +a.r || 0;
-            node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom));
-            o._.dirty = 1;
-        }
-        if ("clip-rect" in params) {
-            var rect = Str(params["clip-rect"]).split(separator);
-            if (rect.length == 4) {
-                rect[2] = +rect[2] + (+rect[0]);
-                rect[3] = +rect[3] + (+rect[1]);
-                var div = node.clipRect || R._g.doc.createElement("div"),
-                    dstyle = div.style;
-                dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect);
-                if (!node.clipRect) {
-                    dstyle.position = "absolute";
-                    dstyle.top = 0;
-                    dstyle.left = 0;
-                    dstyle.width = o.paper.width + "px";
-                    dstyle.height = o.paper.height + "px";
-                    node.parentNode.insertBefore(div, node);
-                    div.appendChild(node);
-                    node.clipRect = div;
-                }
-            }
-            if (!params["clip-rect"]) {
-                node.clipRect && (node.clipRect.style.clip = "auto");
-            }
-        }
-        if (o.textpath) {
-            var textpathStyle = o.textpath.style;
-            params.font && (textpathStyle.font = params.font);
-            params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"');
-            params["font-size"] && (textpathStyle.fontSize = params["font-size"]);
-            params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]);
-            params["font-style"] && (textpathStyle.fontStyle = params["font-style"]);
-        }
-        if ("arrow-start" in params) {
-            addArrow(res, params["arrow-start"]);
-        }
-        if ("arrow-end" in params) {
-            addArrow(res, params["arrow-end"], 1);
-        }
-        if (params.opacity != null || 
-            params["stroke-width"] != null ||
-            params.fill != null ||
-            params.src != null ||
-            params.stroke != null ||
-            params["stroke-width"] != null ||
-            params["stroke-opacity"] != null ||
-            params["fill-opacity"] != null ||
-            params["stroke-dasharray"] != null ||
-            params["stroke-miterlimit"] != null ||
-            params["stroke-linejoin"] != null ||
-            params["stroke-linecap"] != null) {
-            var fill = node.getElementsByTagName(fillString),
-                newfill = false;
-            fill = fill && fill[0];
-            !fill && (newfill = fill = createNode(fillString));
-            if (o.type == "image" && params.src) {
-                fill.src = params.src;
-            }
-            params.fill && (fill.on = true);
-            if (fill.on == null || params.fill == "none" || params.fill === null) {
-                fill.on = false;
-            }
-            if (fill.on && params.fill) {
-                var isURL = Str(params.fill).match(R._ISURL);
-                if (isURL) {
-                    fill.parentNode == node && node.removeChild(fill);
-                    fill.rotate = true;
-                    fill.src = isURL[1];
-                    fill.type = "tile";
-                    var bbox = o.getBBox(1);
-                    fill.position = bbox.x + S + bbox.y;
-                    o._.fillpos = [bbox.x, bbox.y];
-
-                    R._preload(isURL[1], function () {
-                        o._.fillsize = [this.offsetWidth, this.offsetHeight];
-                    });
-                } else {
-                    fill.color = R.getRGB(params.fill).hex;
-                    fill.src = E;
-                    fill.type = "solid";
-                    if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) {
-                        a.fill = "none";
-                        a.gradient = params.fill;
-                        fill.rotate = false;
-                    }
-                }
-            }
-            if ("fill-opacity" in params || "opacity" in params) {
-                var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1);
-                opacity = mmin(mmax(opacity, 0), 1);
-                fill.opacity = opacity;
-                if (fill.src) {
-                    fill.color = "none";
-                }
-            }
-            node.appendChild(fill);
-            var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]),
-            newstroke = false;
-            !stroke && (newstroke = stroke = createNode("stroke"));
-            if ((params.stroke && params.stroke != "none") ||
-                params["stroke-width"] ||
-                params["stroke-opacity"] != null ||
-                params["stroke-dasharray"] ||
-                params["stroke-miterlimit"] ||
-                params["stroke-linejoin"] ||
-                params["stroke-linecap"]) {
-                stroke.on = true;
-            }
-            (params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false);
-            var strokeColor = R.getRGB(params.stroke);
-            stroke.on && params.stroke && (stroke.color = strokeColor.hex);
-            opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1);
-            var width = (toFloat(params["stroke-width"]) || 1) * .75;
-            opacity = mmin(mmax(opacity, 0), 1);
-            params["stroke-width"] == null && (width = a["stroke-width"]);
-            params["stroke-width"] && (stroke.weight = width);
-            width && width < 1 && (opacity *= width) && (stroke.weight = 1);
-            stroke.opacity = opacity;
-        
-            params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
-            stroke.miterlimit = params["stroke-miterlimit"] || 8;
-            params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round");
-            if ("stroke-dasharray" in params) {
-                var dasharray = {
-                    "-": "shortdash",
-                    ".": "shortdot",
-                    "-.": "shortdashdot",
-                    "-..": "shortdashdotdot",
-                    ". ": "dot",
-                    "- ": "dash",
-                    "--": "longdash",
-                    "- .": "dashdot",
-                    "--.": "longdashdot",
-                    "--..": "longdashdotdot"
-                };
-                stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E;
-            }
-            newstroke && node.appendChild(stroke);
-        }
-        if (res.type == "text") {
-            res.paper.canvas.style.display = E;
-            var span = res.paper.span,
-                m = 100,
-                fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/);
-            s = span.style;
-            a.font && (s.font = a.font);
-            a["font-family"] && (s.fontFamily = a["font-family"]);
-            a["font-weight"] && (s.fontWeight = a["font-weight"]);
-            a["font-style"] && (s.fontStyle = a["font-style"]);
-            fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10;
-            s.fontSize = fontSize * m + "px";
-            res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "&#60;").replace(/&/g, "&#38;").replace(/\n/g, "<br>"));
-            var brect = span.getBoundingClientRect();
-            res.W = a.w = (brect.right - brect.left) / m;
-            res.H = a.h = (brect.bottom - brect.top) / m;
-            // res.paper.canvas.style.display = "none";
-            res.X = a.x;
-            res.Y = a.y + res.H / 2;
-
-            ("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1));
-            var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"];
-            for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) {
-                res._.dirty = 1;
-                break;
-            }
-        
-            // text-anchor emulation
-            switch (a["text-anchor"]) {
-                case "start":
-                    res.textpath.style["v-text-align"] = "left";
-                    res.bbx = res.W / 2;
-                break;
-                case "end":
-                    res.textpath.style["v-text-align"] = "right";
-                    res.bbx = -res.W / 2;
-                break;
-                default:
-                    res.textpath.style["v-text-align"] = "center";
-                    res.bbx = 0;
-                break;
-            }
-            res.textpath.style["v-text-kern"] = true;
-        }
-        // res.paper.canvas.style.display = E;
-    },
-    addGradientFill = function (o, gradient, fill) {
-        o.attrs = o.attrs || {};
-        var attrs = o.attrs,
-            pow = Math.pow,
-            opacity,
-            oindex,
-            type = "linear",
-            fxfy = ".5 .5";
-        o.attrs.gradient = gradient;
-        gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) {
-            type = "radial";
-            if (fx && fy) {
-                fx = toFloat(fx);
-                fy = toFloat(fy);
-                pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5);
-                fxfy = fx + S + fy;
-            }
-            return E;
-        });
-        gradient = gradient.split(/\s*\-\s*/);
-        if (type == "linear") {
-            var angle = gradient.shift();
-            angle = -toFloat(angle);
-            if (isNaN(angle)) {
-                return null;
-            }
-        }
-        var dots = R._parseDots(gradient);
-        if (!dots) {
-            return null;
-        }
-        o = o.shape || o.node;
-        if (dots.length) {
-            o.removeChild(fill);
-            fill.on = true;
-            fill.method = "none";
-            fill.color = dots[0].color;
-            fill.color2 = dots[dots.length - 1].color;
-            var clrs = [];
-            for (var i = 0, ii = dots.length; i < ii; i++) {
-                dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color);
-            }
-            fill.colors = clrs.length ? clrs.join() : "0% " + fill.color;
-            if (type == "radial") {
-                fill.type = "gradientTitle";
-                fill.focus = "100%";
-                fill.focussize = "0 0";
-                fill.focusposition = fxfy;
-                fill.angle = 0;
-            } else {
-                // fill.rotate= true;
-                fill.type = "gradient";
-                fill.angle = (270 - angle) % 360;
-            }
-            o.appendChild(fill);
-        }
-        return 1;
-    },
-    Element = function (node, vml) {
-        this[0] = this.node = node;
-        node.raphael = true;
-        this.id = R._oid++;
-        node.raphaelid = this.id;
-        this.X = 0;
-        this.Y = 0;
-        this.attrs = {};
-        this.paper = vml;
-        this.matrix = R.matrix();
-        this._ = {
-            transform: [],
-            sx: 1,
-            sy: 1,
-            dx: 0,
-            dy: 0,
-            deg: 0,
-            dirty: 1,
-            dirtyT: 1
-        };
-        !vml.bottom && (vml.bottom = this);
-        this.prev = vml.top;
-        vml.top && (vml.top.next = this);
-        vml.top = this;
-        this.next = null;
-    };
-    var elproto = R.el;
-
-    Element.prototype = elproto;
-    elproto.constructor = Element;
-    elproto.transform = function (tstr) {
-        if (tstr == null) {
-            return this._.transform;
-        }
-        var vbs = this.paper._viewBoxShift,
-            vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E,
-            oldt;
-        if (vbs) {
-            oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E);
-        }
-        R._extractTransform(this, vbt + tstr);
-        var matrix = this.matrix.clone(),
-            skew = this.skew,
-            o = this.node,
-            split,
-            isGrad = ~Str(this.attrs.fill).indexOf("-"),
-            isPatt = !Str(this.attrs.fill).indexOf("url(");
-        matrix.translate(1, 1);
-        if (isPatt || isGrad || this.type == "image") {
-            skew.matrix = "1 0 0 1";
-            skew.offset = "0 0";
-            split = matrix.split();
-            if ((isGrad && split.noRotation) || !split.isSimple) {
-                o.style.filter = matrix.toFilter();
-                var bb = this.getBBox(),
-                    bbt = this.getBBox(1),
-                    dx = bb.x - bbt.x,
-                    dy = bb.y - bbt.y;
-                o.coordorigin = (dx * -zoom) + S + (dy * -zoom);
-                setCoords(this, 1, 1, dx, dy, 0);
-            } else {
-                o.style.filter = E;
-                setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate);
-            }
-        } else {
-            o.style.filter = E;
-            skew.matrix = Str(matrix);
-            skew.offset = matrix.offset();
-        }
-        if (oldt !== null) { // empty string value is true as well
-            this._.transform = oldt;
-            R._extractTransform(this, oldt);
-        }
-        return this;
-    };
-    elproto.rotate = function (deg, cx, cy) {
-        if (this.removed) {
-            return this;
-        }
-        if (deg == null) {
-            return;
-        }
-        deg = Str(deg).split(separator);
-        if (deg.length - 1) {
-            cx = toFloat(deg[1]);
-            cy = toFloat(deg[2]);
-        }
-        deg = toFloat(deg[0]);
-        (cy == null) && (cx = cy);
-        if (cx == null || cy == null) {
-            var bbox = this.getBBox(1);
-            cx = bbox.x + bbox.width / 2;
-            cy = bbox.y + bbox.height / 2;
-        }
-        this._.dirtyT = 1;
-        this.transform(this._.transform.concat([["r", deg, cx, cy]]));
-        return this;
-    };
-    elproto.translate = function (dx, dy) {
-        if (this.removed) {
-            return this;
-        }
-        dx = Str(dx).split(separator);
-        if (dx.length - 1) {
-            dy = toFloat(dx[1]);
-        }
-        dx = toFloat(dx[0]) || 0;
-        dy = +dy || 0;
-        if (this._.bbox) {
-            this._.bbox.x += dx;
-            this._.bbox.y += dy;
-        }
-        this.transform(this._.transform.concat([["t", dx, dy]]));
-        return this;
-    };
-    elproto.scale = function (sx, sy, cx, cy) {
-        if (this.removed) {
-            return this;
-        }
-        sx = Str(sx).split(separator);
-        if (sx.length - 1) {
-            sy = toFloat(sx[1]);
-            cx = toFloat(sx[2]);
-            cy = toFloat(sx[3]);
-            isNaN(cx) && (cx = null);
-            isNaN(cy) && (cy = null);
-        }
-        sx = toFloat(sx[0]);
-        (sy == null) && (sy = sx);
-        (cy == null) && (cx = cy);
-        if (cx == null || cy == null) {
-            var bbox = this.getBBox(1);
-        }
-        cx = cx == null ? bbox.x + bbox.width / 2 : cx;
-        cy = cy == null ? bbox.y + bbox.height / 2 : cy;
-    
-        this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
-        this._.dirtyT = 1;
-        return this;
-    };
-    elproto.hide = function () {
-        !this.removed && (this.node.style.display = "none");
-        return this;
-    };
-    elproto.show = function () {
-        !this.removed && (this.node.style.display = E);
-        return this;
-    };
-    // Needed to fix the vml setViewBox issues
-    elproto.auxGetBBox = R.el.getBBox;
-    elproto.getBBox = function(){
-      var b = this.auxGetBBox();
-      if (this.paper && this.paper._viewBoxShift)
-      {
-        var c = {};
-        var z = 1/this.paper._viewBoxShift.scale;
-        c.x = b.x - this.paper._viewBoxShift.dx;
-        c.x *= z;
-        c.y = b.y - this.paper._viewBoxShift.dy;
-        c.y *= z;
-        c.width  = b.width  * z;
-        c.height = b.height * z;
-        c.x2 = c.x + c.width;
-        c.y2 = c.y + c.height;
-        return c;
-      }
-      return b;
-    };
-    elproto._getBBox = function () {
-        if (this.removed) {
-            return {};
-        }
-        return {
-            x: this.X + (this.bbx || 0) - this.W / 2,
-            y: this.Y - this.H,
-            width: this.W,
-            height: this.H
-        };
-    };
-    elproto.remove = function () {
-        if (this.removed || !this.node.parentNode) {
-            return;
-        }
-        this.paper.__set__ && this.paper.__set__.exclude(this);
-        R.eve.unbind("raphael.*.*." + this.id);
-        R._tear(this, this.paper);
-        this.node.parentNode.removeChild(this.node);
-        this.shape && this.shape.parentNode.removeChild(this.shape);
-        for (var i in this) {
-            this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
-        }
-        this.removed = true;
-    };
-    elproto.attr = function (name, value) {
-        if (this.removed) {
-            return this;
-        }
-        if (name == null) {
-            var res = {};
-            for (var a in this.attrs) if (this.attrs[has](a)) {
-                res[a] = this.attrs[a];
-            }
-            res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
-            res.transform = this._.transform;
-            return res;
-        }
-        if (value == null && R.is(name, "string")) {
-            if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) {
-                return this.attrs.gradient;
-            }
-            var names = name.split(separator),
-                out = {};
-            for (var i = 0, ii = names.length; i < ii; i++) {
-                name = names[i];
-                if (name in this.attrs) {
-                    out[name] = this.attrs[name];
-                } else if (R.is(this.paper.customAttributes[name], "function")) {
-                    out[name] = this.paper.customAttributes[name].def;
-                } else {
-                    out[name] = R._availableAttrs[name];
-                }
-            }
-            return ii - 1 ? out : out[names[0]];
-        }
-        if (this.attrs && value == null && R.is(name, "array")) {
-            out = {};
-            for (i = 0, ii = name.length; i < ii; i++) {
-                out[name[i]] = this.attr(name[i]);
-            }
-            return out;
-        }
-        var params;
-        if (value != null) {
-            params = {};
-            params[name] = value;
-        }
-        value == null && R.is(name, "object") && (params = name);
-        for (var key in params) {
-            eve("raphael.attr." + key + "." + this.id, this, params[key]);
-        }
-        if (params) {
-            for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
-                var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
-                this.attrs[key] = params[key];
-                for (var subkey in par) if (par[has](subkey)) {
-                    params[subkey] = par[subkey];
-                }
-            }
-            // this.paper.canvas.style.display = "none";
-            if (params.text && this.type == "text") {
-                this.textpath.string = params.text;
-            }
-            setFillAndStroke(this, params);
-            // this.paper.canvas.style.display = E;
-        }
-        return this;
-    };
-    elproto.toFront = function () {
-        !this.removed && this.node.parentNode.appendChild(this.node);
-        this.paper && this.paper.top != this && R._tofront(this, this.paper);
-        return this;
-    };
-    elproto.toBack = function () {
-        if (this.removed) {
-            return this;
-        }
-        if (this.node.parentNode.firstChild != this.node) {
-            this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
-            R._toback(this, this.paper);
-        }
-        return this;
-    };
-    elproto.insertAfter = function (element) {
-        if (this.removed) {
-            return this;
-        }
-        if (element.constructor == R.st.constructor) {
-            element = element[element.length - 1];
-        }
-        if (element.node.nextSibling) {
-            element.node.parentNode.insertBefore(this.node, element.node.nextSibling);
-        } else {
-            element.node.parentNode.appendChild(this.node);
-        }
-        R._insertafter(this, element, this.paper);
-        return this;
-    };
-    elproto.insertBefore = function (element) {
-        if (this.removed) {
-            return this;
-        }
-        if (element.constructor == R.st.constructor) {
-            element = element[0];
-        }
-        element.node.parentNode.insertBefore(this.node, element.node);
-        R._insertbefore(this, element, this.paper);
-        return this;
-    };
-    elproto.blur = function (size) {
-        var s = this.node.runtimeStyle,
-            f = s.filter;
-        f = f.replace(blurregexp, E);
-        if (+size !== 0) {
-            this.attrs.blur = size;
-            s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")";
-            s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5));
-        } else {
-            s.filter = f;
-            s.margin = 0;
-            delete this.attrs.blur;
-        }
-        return this;
-    };
-
-    R._engine.path = function (pathString, vml) {
-        var el = createNode("shape");
-        el.style.cssText = cssDot;
-        el.coordsize = zoom + S + zoom;
-        el.coordorigin = vml.coordorigin;
-        var p = new Element(el, vml),
-            attr = {fill: "none", stroke: "#000"};
-        pathString && (attr.path = pathString);
-        p.type = "path";
-        p.path = [];
-        p.Path = E;
-        setFillAndStroke(p, attr);
-        vml.canvas.appendChild(el);
-        var skew = createNode("skew");
-        skew.on = true;
-        el.appendChild(skew);
-        p.skew = skew;
-        p.transform(E);
-        return p;
-    };
-    R._engine.rect = function (vml, x, y, w, h, r) {
-        var path = R._rectPath(x, y, w, h, r),
-            res = vml.path(path),
-            a = res.attrs;
-        res.X = a.x = x;
-        res.Y = a.y = y;
-        res.W = a.width = w;
-        res.H = a.height = h;
-        a.r = r;
-        a.path = path;
-        res.type = "rect";
-        return res;
-    };
-    R._engine.ellipse = function (vml, x, y, rx, ry) {
-        var res = vml.path(),
-            a = res.attrs;
-        res.X = x - rx;
-        res.Y = y - ry;
-        res.W = rx * 2;
-        res.H = ry * 2;
-        res.type = "ellipse";
-        setFillAndStroke(res, {
-            cx: x,
-            cy: y,
-            rx: rx,
-            ry: ry
-        });
-        return res;
-    };
-    R._engine.circle = function (vml, x, y, r) {
-        var res = vml.path(),
-            a = res.attrs;
-        res.X = x - r;
-        res.Y = y - r;
-        res.W = res.H = r * 2;
-        res.type = "circle";
-        setFillAndStroke(res, {
-            cx: x,
-            cy: y,
-            r: r
-        });
-        return res;
-    };
-    R._engine.image = function (vml, src, x, y, w, h) {
-        var path = R._rectPath(x, y, w, h),
-            res = vml.path(path).attr({stroke: "none"}),
-            a = res.attrs,
-            node = res.node,
-            fill = node.getElementsByTagName(fillString)[0];
-        a.src = src;
-        res.X = a.x = x;
-        res.Y = a.y = y;
-        res.W = a.width = w;
-        res.H = a.height = h;
-        a.path = path;
-        res.type = "image";
-        fill.parentNode == node && node.removeChild(fill);
-        fill.rotate = true;
-        fill.src = src;
-        fill.type = "tile";
-        res._.fillpos = [x, y];
-        res._.fillsize = [w, h];
-        node.appendChild(fill);
-        setCoords(res, 1, 1, 0, 0, 0);
-        return res;
-    };
-    R._engine.text = function (vml, x, y, text) {
-        var el = createNode("shape"),
-            path = createNode("path"),
-            o = createNode("textpath");
-        x = x || 0;
-        y = y || 0;
-        text = text || "";
-        path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1);
-        path.textpathok = true;
-        o.string = Str(text);
-        o.on = true;
-        el.style.cssText = cssDot;
-        el.coordsize = zoom + S + zoom;
-        el.coordorigin = "0 0";
-        var p = new Element(el, vml),
-            attr = {
-                fill: "#000",
-                stroke: "none",
-                font: R._availableAttrs.font,
-                text: text
-            };
-        p.shape = el;
-        p.path = path;
-        p.textpath = o;
-        p.type = "text";
-        p.attrs.text = Str(text);
-        p.attrs.x = x;
-        p.attrs.y = y;
-        p.attrs.w = 1;
-        p.attrs.h = 1;
-        setFillAndStroke(p, attr);
-        el.appendChild(o);
-        el.appendChild(path);
-        vml.canvas.appendChild(el);
-        var skew = createNode("skew");
-        skew.on = true;
-        el.appendChild(skew);
-        p.skew = skew;
-        p.transform(E);
-        return p;
-    };
-    R._engine.setSize = function (width, height) {
-        var cs = this.canvas.style;
-        this.width = width;
-        this.height = height;
-        width == +width && (width += "px");
-        height == +height && (height += "px");
-        cs.width = width;
-        cs.height = height;
-        cs.clip = "rect(0 " + width + " " + height + " 0)";
-        if (this._viewBox) {
-            R._engine.setViewBox.apply(this, this._viewBox);
-        }
-        return this;
-    };
-    R._engine.setViewBox = function (x, y, w, h, fit) {
-        R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
-        var paperSize = this.getSize(),
-            width = paperSize.width,
-            height = paperSize.height,
-            H, W;
-        if (fit) {
-            H = height / h;
-            W = width / w;
-            if (w * H < width) {
-                x -= (width - w * H) / 2 / H;
-            }
-            if (h * W < height) {
-                y -= (height - h * W) / 2 / W;
-            }
-        }
-        this._viewBox = [x, y, w, h, !!fit];
-        this._viewBoxShift = {
-            dx: -x,
-            dy: -y,
-            scale: paperSize
-        };
-        this.forEach(function (el) {
-            el.transform("...");
-        });
-        return this;
-    };
-    var createNode;
-    R._engine.initWin = function (win) {
-            var doc = win.document;
-            if (doc.styleSheets.length < 31) {
-                doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
-            } else {
-                // no more room, add to the existing one
-                // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx
-                doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)");
-            }
-            try {
-                !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
-                createNode = function (tagName) {
-                    return doc.createElement('<rvml:' + tagName + ' class="rvml">');
-                };
-            } catch (e) {
-                createNode = function (tagName) {
-                    return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
-                };
-            }
-        };
-    R._engine.initWin(R._g.win);
-    R._engine.create = function () {
-        var con = R._getContainer.apply(0, arguments),
-            container = con.container,
-            height = con.height,
-            s,
-            width = con.width,
-            x = con.x,
-            y = con.y;
-        if (!container) {
-            throw new Error("VML container not found.");
-        }
-        var res = new R._Paper,
-            c = res.canvas = R._g.doc.createElement("div"),
-            cs = c.style;
-        x = x || 0;
-        y = y || 0;
-        width = width || 512;
-        height = height || 342;
-        res.width = width;
-        res.height = height;
-        width == +width && (width += "px");
-        height == +height && (height += "px");
-        res.coordsize = zoom * 1e3 + S + zoom * 1e3;
-        res.coordorigin = "0 0";
-        res.span = R._g.doc.createElement("span");
-        res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;";
-        c.appendChild(res.span);
-        cs.cssText = R.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden", width, height);
-        if (container == 1) {
-            R._g.doc.body.appendChild(c);
-            cs.left = x + "px";
-            cs.top = y + "px";
-            cs.position = "absolute";
-        } else {
-            if (container.firstChild) {
-                container.insertBefore(c, container.firstChild);
-            } else {
-                container.appendChild(c);
-            }
-        }
-        res.renderfix = function () {};
-        return res;
-    };
-    R.prototype.clear = function () {
-        R.eve("raphael.clear", this);
-        this.canvas.innerHTML = E;
-        this.span = R._g.doc.createElement("span");
-        this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
-        this.canvas.appendChild(this.span);
-        this.bottom = this.top = null;
-    };
-    R.prototype.remove = function () {
-        R.eve("raphael.remove", this);
-        this.canvas.parentNode.removeChild(this.canvas);
-        for (var i in this) {
-            this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
-        }
-        return true;
-    };
-
-    var setproto = R.st;
-    for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
-        setproto[method] = (function (methodname) {
-            return function () {
-                var arg = arguments;
-                return this.forEach(function (el) {
-                    el[methodname].apply(el, arg);
-                });
-            };
-        })(method);
-    }
-})();
-
-    // EXPOSE
-    // SVG and VML are appended just before the EXPOSE line
-    // Even with AMD, Raphael should be defined globally
-    oldRaphael.was ? (g.win.Raphael = R) : (Raphael = R);
-
-    if(typeof exports == "object"){
-        module.exports = R;
-    }
-    return R;
-}));
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index a1a65c2d72ee5064c480bdd63eea9e2f49248a1d..520a86352f74b3c3f8959ec7bdb1630332a70362 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -37,6 +37,7 @@ captures/
 .idea/workspace.xml
 .idea/tasks.xml
 .idea/gradle.xml
+.idea/dictionaries
 .idea/libraries
 
 # Keystore files
@@ -48,7 +49,7 @@ captures/
 # Google Services (e.g. APIs or Firebase)
 google-services.json
 
-#Freeline
+# Freeline
 freeline.py
 freeline/
 freeline_project_description.json
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 31c9fb31167aa514b6c897eddbc6603e477c32ff..4f88399d2d82ed9af72a39bbfcd6ded2989eccd4 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -49,3 +49,8 @@ local.properties
 
 # Code Recommenders
 .recommenders/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 401fee1574896c6b5b834e8b52b162a4b08366f1..ec7e95c6ab5a369f120450356d60d52efcd02f77 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,6 +4,7 @@
 # User-specific stuff:
 .idea/**/workspace.xml
 .idea/**/tasks.xml
+.idea/dictionaries
 
 # Sensitive or high-churn files:
 .idea/**/dataSources/
diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore
index 970d897c75cee1d88fb6e021e8819a9bb446cd99..5ed6acb6576d40c10ca2b6b96ade7302eeb5c026 100644
--- a/vendor/gitignore/Global/SBT.gitignore
+++ b/vendor/gitignore/Global/SBT.gitignore
@@ -1,9 +1,12 @@
 # Simple Build Tool
 # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
 
+dist/*
 target/
 lib_managed/
 src_managed/
 project/boot/
+project/plugins/project/
 .history
 .cache
+.lib/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index dbb4a2dfa1a4729f5eb45daa298c5869a1d7083d..6143e53f9e36d08e4da5d743017e2c04ca91d86d 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,3 +1,4 @@
+# Compiled class file
 *.class
 
 # Log file
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 9af45b175ae8a1640c37d2795dcb8f16cdbe9a35..5f2dbe11df91c879164ae1150f5577ac7263c1da 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -8,5 +8,5 @@ dependency-reduced-pom.xml
 buildNumber.properties
 .mvn/timing.properties
 
-# Exclude maven wrapper
+# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
 !/.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 38ac77e405e28f554517618aea09dbce5b29ea4c..00cbbdf53f6c487c8392d936ce7225824c11446e 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -2,6 +2,8 @@
 logs
 *.log
 npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
 
 # Runtime data
 pids
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index af90c007a3f70f59b051944baf8c07b82b7f56d6..09dfede48147fd815b17e60a81ab7c4db65fd0a8 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -45,10 +45,10 @@ Carthage/Build
 
 # fastlane
 #
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
 # screenshots whenever they are needed.
 # For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
 
 fastlane/report.xml
 fastlane/Preview.html
diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore
index 6d67f1191751bc6de7f13d7c7c10399c31c7b38b..ae5ec9fe1d9fb888c1ab3d2fac9fe15868505e5c 100644
--- a/vendor/gitignore/PlayFramework.gitignore
+++ b/vendor/gitignore/PlayFramework.gitignore
@@ -5,6 +5,7 @@ bin/
 /lib/
 /logs/
 /modules
+/project/project
 /project/target
 /target
 tmp/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index cf3102d6b00e55b0cc23a4b7ccbe44bc522d42b8..62c1e736924fba083ec522ac0318392581ffc103 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -76,6 +76,9 @@ target/
 # celery beat schedule file
 celerybeat-schedule
 
+# SageMath parsed files
+*.sage.py
+
 # dotenv
 .env
 
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index 006a7b247fefe09936ffddd7da25b1a042237a8f..9c07d4ae98846cc6160759718b195afc884ee605 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -1,23 +1,2 @@
 *.class
 *.log
-
-# sbt specific
-.cache
-.history
-.lib/
-dist/*
-target/
-lib_managed/
-src_managed/
-project/boot/
-project/plugins/project/
-
-# Scala-IDE specific
-.ensime
-.ensime_cache/
-.scala_dependencies
-.worksheet
-
-# ENSIME specific
-.ensime_cache/
-.ensime
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 099d22ae2f40a88d381f7914c9ebd7ba55d064fb..d53404493965e7fa24968507f28ed46d6c37f702 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -59,7 +59,7 @@ Carthage/Build
 # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
 # screenshots whenever they are needed.
 # For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
 
 fastlane/report.xml
 fastlane/Preview.html
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index ed4d3c6c28dcecca9c235c55dee4421b21307a97..6c224e024e95fb06a53c67c85c8b73b1fe40265a 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -25,7 +25,6 @@
 /bin/*
 !bin/console
 !bin/symfony_requirements
-/vendor/
 
 # Assets and user uploads
 /web/bundles/
@@ -38,8 +37,5 @@
 # Build data
 /build/
 
-# Composer PHAR
-/composer.phar
-
 # Backup entities generated with doctrine:generate:entities command
 **/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 69bfb1eec3e5e8b95b5af3aa52f522e0b9b2ab25..57ed9f5d97227fe070d9cf10e18f765474544b51 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -28,7 +28,6 @@
 *.blg
 *-blx.aux
 *-blx.bib
-*.brf
 *.run.xml
 
 ## Build tool auxiliary files:
@@ -77,8 +76,6 @@ acs-*.bib
 *.t[1-9]
 *.t[1-9][0-9]
 *.tfm
-*.[1-9]
-*.[1-9][0-9]
 
 #(r)(e)ledmac/(r)(e)ledpar
 *.end
@@ -134,6 +131,9 @@ acs-*.bib
 *.mlf
 *.mlt
 *.mtc[0-9]*
+*.slf[0-9]*
+*.slt[0-9]*
+*.stc[0-9]*
 
 # minted
 _minted*
@@ -142,9 +142,6 @@ _minted*
 # morewrites
 *.mw
 
-# mylatexformat
-*.fmt
-
 # nomencl
 *.nlo
 
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 8054980d742a809bec19753e75ca8496f11f43c7..a752eacca7decc2fdf37befd97a2610df9f7af11 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -166,7 +166,7 @@ PublishScripts/
 !**/packages/build/
 # Uncomment if necessary however generally it will be regenerated when needed
 #!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable files
+# NuGet v3's project.json files produces more ignorable files
 *.nuget.props
 *.nuget.targets
 
@@ -276,3 +276,12 @@ __pycache__/
 # Cake - Uncomment if you are using it
 # tools/**
 # !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Android.gitlab-ci.yml b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5f9d54ff5745e549f749081787d1d6a04a2396de
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
@@ -0,0 +1,51 @@
+# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli
+image: openjdk:8-jdk
+
+variables:
+  ANDROID_COMPILE_SDK: "25"
+  ANDROID_BUILD_TOOLS: "24.0.0"
+  ANDROID_SDK_TOOLS: "24.4.1"
+
+before_script:
+  - apt-get --quiet update --yes
+  - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
+  - wget --quiet --output-document=android-sdk.tgz https://dl.google.com/android/android-sdk_r${ANDROID_SDK_TOOLS}-linux.tgz
+  - tar --extract --gzip --file=android-sdk.tgz
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_COMPILE_SDK}
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS}
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services
+  - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository
+  - export ANDROID_HOME=$PWD/android-sdk-linux
+  - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
+  - chmod +x ./gradlew
+
+stages:
+  - build
+  - test
+
+build:
+  stage: build
+  script:
+    - ./gradlew assembleDebug
+  artifacts:
+    paths:
+    - app/build/outputs/
+
+unitTests:
+  stage: test
+  script:
+    - ./gradlew test
+
+functionalTests:
+  stage: test
+  script:
+    - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
+    - chmod +x android-wait-for-emulator
+    - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK}
+    - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86
+    - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio &
+    - ./android-wait-for-emulator
+    - adb shell input keyevent 82
+    - ./gradlew cAT
diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..27537689b8076eba8dc1635c4a2ec4ef5acb1339
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
@@ -0,0 +1,35 @@
+# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options
+
+# you can delete this line if you're not using Docker
+image: busybox:latest
+
+before_script:
+   - echo "Before script section"
+   - echo "For example you might run an update here or install a build dependency"
+   - echo "Or perhaps you might print out some debugging details"
+   
+after_script:
+  - echo "After script section"
+  - echo "For example you might do some cleanup here"
+   
+build1:
+ stage: build
+ script:
+   - echo "Do your build here"
+   
+test1:
+ stage: test
+ script: 
+   - echo "Do a test here"
+   - echo "For example run a test suite"
+   
+test2:
+ stage: test
+ script: 
+   - echo "Do another parallel test here"
+   - echo "For example run a lint test"
+   
+deploy1:
+ stage: deploy
+ script:
+   - echo "Do your deploy here"
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index e8da49a935e3e90c4641004edeb02cfc38c9eb64..37e44735f7cc03860031c1f202032b5b84d9bea7 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -1,4 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
 # Official language image. Look for the different tagged releases at:
 # https://hub.docker.com/r/crystallang/crystal/
 image: "crystallang/crystal:latest"
diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b3106863ccafa189a674b2a42ddd6c4e5185ffb4
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/python
+image: python:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+  - mysql:latest
+  - postgres:latest
+
+variables:
+  POSTGRES_DB: database_name
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+  paths:
+  - ~/.cache/pip/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+  - python -V                                   # Print out python version for debugging
+  # Uncomment next line if your Django app needs a JS runtime:
+  # - apt-get update -q && apt-get install nodejs -yqq
+  - pip install -r requirements.txt
+
+test:
+  variables:
+    DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
+  script:
+  - python manage.py migrate
+  - python manage.py test
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 8c5905799346c7d03cf1f27093fb38ef579f6e9f..40648bcd3deb1e000c8443e74a1c5e49f00561bc 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -7,7 +7,7 @@ services:
 build:
   stage: build
   script:
-    - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-')
-    - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
+    - export IMAGE_TAG=$(echo -en $CI_COMMIT_REF_NAME | tr -c '[:alnum:]_.-' '-')
+    - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY
     - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .
     - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG"
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index 98d3039ad06d9097c003d0cadf34ecc9d8356b13..a65e48a3389f01695ecf8fa102c23ac2a4a72275 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -6,6 +6,13 @@
 # https://github.com/gradle/gradle
 image: java:8
 
+# Disable the Gradle daemon for Continuous Integration servers as correctness
+# is usually a priority over speed in CI environments. Using a fresh
+# runtime for each build is more reliable since the runtime is completely
+# isolated from any previous builds.
+variables:
+    GRADLE_OPTS: "-Dorg.gradle.daemon=false"
+
 # Make the gradle wrapper executable. This essentially downloads a copy of
 # Gradle to build the project with.
 # https://docs.gradle.org/current/userguide/gradle_wrapper.html
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
index 80f7b87b6c04506dee904f6840c857989d9b2da1..d6c93c6fcf775605c4d7066cb976f3ac11ee9c83 100644
--- a/vendor/gitlab-ci-yml/LICENSE
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016 GitLab.org
+Copyright (c) 2016-2017 GitLab.org
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0d6a6eddc9714aa2786187524a926b74442b88d4
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
@@ -0,0 +1,78 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/php
+image: php:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+  - mysql:latest
+
+variables:
+  MYSQL_DATABASE: project_name
+  MYSQL_ROOT_PASSWORD: secret
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+  paths:
+  - vendor/
+  - node_modules/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+  # Update packages 
+  - apt-get update -yqq
+  
+  # Upgrade to Node 7
+  - curl -sL https://deb.nodesource.com/setup_7.x | bash -
+  
+  # Install dependencies
+  - apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
+
+  # Install php extensions
+  - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache
+
+  # Install Composer and project dependencies.
+  - curl -sS https://getcomposer.org/installer | php
+  - php composer.phar install 
+
+  # Install Node dependencies.
+  # comment this out if you don't have a node dependency
+  - npm install
+
+  # Copy over testing configuration.
+  # Don't forget to set the database config in .env.testing correctly
+  # DB_HOST=mysql
+  # DB_DATABASE=project_name
+  # DB_USERNAME=root
+  # DB_PASSWORD=secret
+  - cp .env.testing .env
+
+  # Run npm build
+  # comment this out if you don't have a frontend build
+  # you can change this to to your frontend building script like
+  # npm run build
+  - npm run dev
+
+  # Generate an application key. Re-cache.
+  - php artisan key:generate
+  - php artisan config:cache
+
+  # Run database migrations.
+  - php artisan migrate
+
+  # Run database seed
+  - php artisan db:seed
+
+test:
+  script:
+  # run laravel tests
+  - php vendor/bin/phpunit --coverage-text --colors=never 
+
+  # run frontend tests
+  # if you have any task for testing frontend
+  # set it in your package.json script
+  # comment this out if you don't have a frontend test
+  - npm test
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index 1678a47f9acc24c4c2d171626d71ca6516f48917..91b096654d10beebf0b488651022c18a1c461604 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -3,9 +3,9 @@
 # For docker image tags see https://hub.docker.com/_/maven/
 #
 # For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
-# 
+#
 # This template will build and test your projects as well as create the documentation.
-# 
+#
 # * Caches downloaded dependencies and plugins between invocation.
 # * Does only verify merge requests but deploy built artifacts of the
 #   master branch.
@@ -17,18 +17,19 @@
 variables:
   # This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
   # `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
-  MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
+  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
   # As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
   # when running from the command line.
   # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
   MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
 
 # Cache downloaded dependencies and plugins between builds.
+# To keep cache across branches add 'key: "$CI_JOB_REF_NAME"'
 cache:
   paths:
-    - /root/.m2/repository/
+    - .m2/repository
 
-# This will only validate and compile stuff and run e.g. maven-enforcer-plugin. 
+# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
 # Because some enforcer rules might check dependency convergence and class duplications
 # we use `test-compile` here instead of `validate`, so the correct classpath is picked up.
 .validate: &validate
diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
similarity index 79%
rename from vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
rename to vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
index 2ba5cad96822157fdedc14615a0e8f38d7dfbf36..d3bb388a1e73c7e2a08959908b015026f6b1c656 100644
--- a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -1,4 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
 image: ayufan/openshift-cli
 
 stages:
@@ -6,6 +5,7 @@ stages:
   - review
   - staging
   - production
+  - cleanup
 
 variables:
   OPENSHIFT_SERVER: openshift.default.svc.cluster.local
@@ -28,7 +28,7 @@ test2:
 .deploy: &deploy
   before_script:
     - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
-    - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME"
+    - oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID"
   script:
     - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
     - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
@@ -38,11 +38,11 @@ review:
   <<: *deploy
   stage: review
   variables:
-    APP: $CI_BUILD_REF_NAME
-    APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+    APP: $CI_COMMIT_REF_NAME
+    APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
   environment:
-    name: review/$CI_BUILD_REF_NAME
-    url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+    name: review/$CI_COMMIT_REF_SLUG
+    url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
     on_stop: stop-review
   only:
     - branches
@@ -51,15 +51,15 @@ review:
 
 stop-review:
   <<: *deploy
-  stage: review
+  stage: cleanup
   script:
     - oc delete all -l "app=$APP"
   when: manual
   variables:
-    APP: $CI_BUILD_REF_NAME
+    APP: $CI_COMMIT_REF_NAME
     GIT_STRATEGY: none
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_SLUG
     action: stop
   only:
     - branches
diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bb8caa49d6b996440f641acc79c50d2993154731
--- /dev/null
+++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
@@ -0,0 +1,33 @@
+# Select image from https://hub.docker.com/_/php/
+image: php:7.1.1
+
+# Select what we should cache between builds
+cache:
+  paths:
+  - vendor/
+
+before_script:
+- apt-get update -yqq
+- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
+# Install PHP extensions
+- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
+# Install and run Composer
+- curl -sS https://getcomposer.org/installer | php
+- php composer.phar install
+
+# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# See http://docs.gitlab.com/ce/ci/services/README.html for examples.
+services:
+  - mysql:5.7
+
+# Set any variables we need
+variables:
+  # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/)
+  MYSQL_DATABASE: mysql_database
+  MYSQL_ROOT_PASSWORD: mysql_strong_password
+
+# Run our tests
+# If Xdebug was installed you can generate a coverage report and see code coverage metrics.
+test:
+  script:
+  - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
index 45df69752594922806a27b038cc50042ec71161b..a72b82814016c6fbc7116646f1b4c267cdb90fbd 100644
--- a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
@@ -9,3 +9,9 @@ pages:
     - public
   only:
   - master
+  
+test:
+  script:
+  - hugo
+  except:
+  - master
diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
index 36918fc005a15b47de1cbd71e3748bd5babe392e..d98cf94d63592f6ea86f6cefd43160f63d88b18f 100644
--- a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
@@ -1,11 +1,15 @@
-# Full project: https://gitlab.com/pages/jekyll
+# Template project: https://gitlab.com/pages/jekyll
+# Docs: https://docs.gitlab.com/ce/pages/
+# Jekyll version: 3.4.0
 image: ruby:2.3
 
+before_script:
+- bundle install
+
 test:
   stage: test
   script:
-  - gem install jekyll
-  - jekyll build -d test
+  - bundle exec jekyll build -d test
   artifacts:
     paths:
     - test
@@ -15,10 +19,10 @@ test:
 pages:
   stage: deploy
   script:
-  - gem install jekyll
-  - jekyll build -d public
+  - bundle exec jekyll build -d public
   artifacts:
     paths:
     - public
   only:
   - master
+  
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index 7298ea73bab9b082650eda7066ee8a66b52a631d..c644560647f029cb3b4d21973a284b4fa9078891 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
   - review
   - staging
   - production
+  - cleanup
 
 build:
   stage: build
@@ -23,12 +24,12 @@ build:
 production:
   stage: production
   variables:
-    CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
     name: production
-    url: http://production.$KUBE_DOMAIN
+    url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
   when: manual
   only:
     - master
@@ -36,24 +37,24 @@ production:
 staging:
   stage: staging
   variables:
-    CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
     name: staging
-    url: http://staging.$KUBE_DOMAIN
+    url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
   only:
     - master
 
 review:
   stage: review
   variables:
-    CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
-    name: review/$CI_BUILD_REF_NAME
-    url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+    name: review/$CI_COMMIT_REF_NAME
+    url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
     on_stop: stop_review
   only:
     - branches
@@ -61,13 +62,13 @@ review:
     - master
 
 stop_review:
-  stage: review
+  stage: cleanup
   variables:
     GIT_STRATEGY: none
   script:
     - command destroy
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     action: stop
   when: manual
   only:
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
index 249adbc9f4ae93afa59cdab285f6a1616370b116..27c9107e0d795ca6eeed840eb711a5e02eac9ec1 100644
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# Explaination on the scripts: 
+# Explanation on the scripts:
 # https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
 image: registry.gitlab.com/gitlab-examples/openshift-deploy
 
@@ -12,6 +12,7 @@ stages:
   - review
   - staging
   - production
+  - cleanup
 
 build:
   stage: build
@@ -23,12 +24,12 @@ build:
 production:
   stage: production
   variables:
-    CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
     name: production
-    url: http://production.$KUBE_DOMAIN
+    url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
   when: manual
   only:
     - master
@@ -36,24 +37,24 @@ production:
 staging:
   stage: staging
   variables:
-    CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
     name: staging
-    url: http://staging.$KUBE_DOMAIN
+    url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
   only:
     - master
 
 review:
   stage: review
   variables:
-    CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+    CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
   script:
     - command deploy
   environment:
-    name: review/$CI_BUILD_REF_NAME
-    url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+    name: review/$CI_COMMIT_REF_NAME
+    url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
     on_stop: stop_review
   only:
     - branches
@@ -61,13 +62,13 @@ review:
     - master
 
 stop_review:
-  stage: review
+  stage: cleanup
   variables:
     GIT_STRATEGY: none
   script:
     - command destroy
   environment:
-    name: review/$CI_BUILD_REF_NAME
+    name: review/$CI_COMMIT_REF_NAME
     action: stop
   when: manual
   only:
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
new file mode 100644
index 0000000000000000000000000000000000000000..a2cbef126ada1fc508e53a55238bee0c522cedea
--- /dev/null
+++ b/vendor/licenses.csv
@@ -0,0 +1,945 @@
+RedCloth,4.3.2,MIT
+abbrev,1.0.9,ISC
+accepts,1.3.3,MIT
+ace-rails-ap,4.1.0,MIT
+acorn,4.0.4,MIT
+acorn-dynamic-import,2.0.1,MIT
+acorn-jsx,3.0.1,MIT
+actionmailer,4.2.8,MIT
+actionpack,4.2.8,MIT
+actionview,4.2.8,MIT
+activejob,4.2.8,MIT
+activemodel,4.2.8,MIT
+activerecord,4.2.8,MIT
+activesupport,4.2.8,MIT
+acts-as-taggable-on,4.0.0,MIT
+addressable,2.3.8,Apache 2.0
+after,0.8.2,MIT
+after_commit_queue,1.3.0,MIT
+ajv,4.11.2,MIT
+ajv-keywords,1.5.1,MIT
+akismet,2.0.0,MIT
+align-text,0.1.4,MIT
+allocations,1.0.5,MIT
+amdefine,1.0.1,BSD-3-Clause OR MIT
+ansi-escapes,1.4.0,MIT
+ansi-html,0.0.7,Apache 2.0
+ansi-regex,2.1.1,MIT
+ansi-styles,2.2.1,MIT
+anymatch,1.3.0,ISC
+append-transform,0.4.0,MIT
+aproba,1.1.0,ISC
+are-we-there-yet,1.1.2,ISC
+arel,6.0.4,MIT
+argparse,1.0.9,MIT
+arr-diff,2.0.0,MIT
+arr-flatten,1.0.1,MIT
+array-find,1.0.0,MIT
+array-flatten,1.1.1,MIT
+array-slice,0.2.3,MIT
+array-union,1.0.2,MIT
+array-uniq,1.0.3,MIT
+array-unique,0.2.1,MIT
+arraybuffer.slice,0.0.6,MIT
+arrify,1.0.1,MIT
+asana,0.4.0,MIT
+asciidoctor,1.5.3,MIT
+asciidoctor-plantuml,0.0.7,MIT
+asn1,0.2.3,MIT
+asn1.js,4.9.1,MIT
+assert,1.4.1,MIT
+assert-plus,0.2.0,MIT
+async,0.2.10,MIT
+async-each,1.0.1,MIT
+asynckit,0.4.0,MIT
+attr_encrypted,3.0.3,MIT
+attr_required,1.0.0,MIT
+autoparse,0.3.3,Apache 2.0
+autoprefixer-rails,6.2.3,MIT
+aws-sign2,0.6.0,Apache 2.0
+aws4,1.6.0,MIT
+axiom-types,0.1.1,MIT
+babel-code-frame,6.22.0,MIT
+babel-core,6.23.1,MIT
+babel-generator,6.23.0,MIT
+babel-helper-bindify-decorators,6.22.0,MIT
+babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
+babel-helper-call-delegate,6.22.0,MIT
+babel-helper-define-map,6.23.0,MIT
+babel-helper-explode-assignable-expression,6.22.0,MIT
+babel-helper-explode-class,6.22.0,MIT
+babel-helper-function-name,6.23.0,MIT
+babel-helper-get-function-arity,6.22.0,MIT
+babel-helper-hoist-variables,6.22.0,MIT
+babel-helper-optimise-call-expression,6.23.0,MIT
+babel-helper-regex,6.22.0,MIT
+babel-helper-remap-async-to-generator,6.22.0,MIT
+babel-helper-replace-supers,6.23.0,MIT
+babel-helpers,6.23.0,MIT
+babel-loader,6.2.10,MIT
+babel-messages,6.23.0,MIT
+babel-plugin-check-es2015-constants,6.22.0,MIT
+babel-plugin-istanbul,4.0.0,New BSD
+babel-plugin-syntax-async-functions,6.13.0,MIT
+babel-plugin-syntax-async-generators,6.13.0,MIT
+babel-plugin-syntax-class-properties,6.13.0,MIT
+babel-plugin-syntax-decorators,6.13.0,MIT
+babel-plugin-syntax-dynamic-import,6.18.0,MIT
+babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
+babel-plugin-syntax-object-rest-spread,6.13.0,MIT
+babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
+babel-plugin-transform-async-generator-functions,6.22.0,MIT
+babel-plugin-transform-async-to-generator,6.22.0,MIT
+babel-plugin-transform-class-properties,6.23.0,MIT
+babel-plugin-transform-decorators,6.22.0,MIT
+babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
+babel-plugin-transform-es2015-classes,6.23.0,MIT
+babel-plugin-transform-es2015-computed-properties,6.22.0,MIT
+babel-plugin-transform-es2015-destructuring,6.23.0,MIT
+babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
+babel-plugin-transform-es2015-for-of,6.23.0,MIT
+babel-plugin-transform-es2015-function-name,6.22.0,MIT
+babel-plugin-transform-es2015-literals,6.22.0,MIT
+babel-plugin-transform-es2015-modules-amd,6.22.0,MIT
+babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-umd,6.23.0,MIT
+babel-plugin-transform-es2015-object-super,6.22.0,MIT
+babel-plugin-transform-es2015-parameters,6.23.0,MIT
+babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
+babel-plugin-transform-es2015-spread,6.22.0,MIT
+babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT
+babel-plugin-transform-es2015-template-literals,6.22.0,MIT
+babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
+babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT
+babel-plugin-transform-exponentiation-operator,6.22.0,MIT
+babel-plugin-transform-object-rest-spread,6.23.0,MIT
+babel-plugin-transform-regenerator,6.22.0,MIT
+babel-plugin-transform-strict-mode,6.22.0,MIT
+babel-preset-es2015,6.22.0,MIT
+babel-preset-stage-2,6.22.0,MIT
+babel-preset-stage-3,6.22.0,MIT
+babel-register,6.23.0,MIT
+babel-runtime,6.22.0,MIT
+babel-template,6.23.0,MIT
+babel-traverse,6.23.1,MIT
+babel-types,6.23.0,MIT
+babosa,1.0.2,MIT
+babylon,6.15.0,MIT
+backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
+base32,0.3.2,MIT
+base64-arraybuffer,0.1.5,MIT
+base64-js,1.2.0,MIT
+base64id,1.0.0,MIT
+batch,0.5.3,MIT
+bcrypt,3.1.11,MIT
+bcrypt-pbkdf,1.0.1,New BSD
+better-assert,1.0.2,MIT
+big.js,3.1.3,MIT
+binary-extensions,1.8.0,MIT
+bindata,2.3.5,ruby
+blob,0.0.4,unknown
+block-stream,0.0.9,ISC
+bluebird,3.4.7,MIT
+bn.js,4.11.6,MIT
+body-parser,1.16.0,MIT
+boom,2.10.1,New BSD
+bootstrap-sass,3.3.6,MIT
+brace-expansion,1.1.6,MIT
+braces,1.8.5,MIT
+brorand,1.0.7,MIT
+browser,2.2.0,MIT
+browserify-aes,1.0.6,MIT
+browserify-cipher,1.0.0,MIT
+browserify-des,1.0.0,MIT
+browserify-rsa,4.0.1,MIT
+browserify-sign,4.0.0,ISC
+browserify-zlib,0.1.4,MIT
+buffer,4.9.1,MIT
+buffer-shims,1.0.0,MIT
+buffer-xor,1.0.3,MIT
+builder,3.2.3,MIT
+builtin-modules,1.1.1,MIT
+builtin-status-codes,3.0.0,MIT
+bytes,2.4.0,MIT
+caller-path,0.1.0,MIT
+callsite,1.0.0,unknown
+callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+carrierwave,0.11.2,MIT
+caseless,0.11.0,Apache 2.0
+cause,0.1,MIT
+center-align,0.1.3,MIT
+chalk,1.1.3,MIT
+charlock_holmes,0.7.3,MIT
+chokidar,1.6.1,MIT
+chronic,0.10.2,MIT
+chronic_duration,0.10.6,MIT
+chunky_png,1.3.5,MIT
+cipher-base,1.0.3,MIT
+circular-json,0.3.1,MIT
+cli-cursor,1.0.2,MIT
+cli-width,2.1.0,ISC
+cliui,2.1.0,ISC
+clone,1.0.2,MIT
+co,4.6.0,MIT
+code-point-at,1.1.0,MIT
+coercible,1.0.0,MIT
+coffee-rails,4.1.1,MIT
+coffee-script,2.4.1,MIT
+coffee-script-source,1.10.0,MIT
+colors,1.1.2,MIT
+combine-lists,1.0.1,MIT
+combined-stream,1.0.5,MIT
+commander,2.9.0,MIT
+commondir,1.0.1,MIT
+component-bind,1.0.0,unknown
+component-emitter,1.2.1,MIT
+component-inherit,0.0.3,unknown
+compressible,2.0.9,MIT
+compression,1.6.2,MIT
+compression-webpack-plugin,0.3.2,MIT
+concat-map,0.0.1,MIT
+concat-stream,1.6.0,MIT
+concurrent-ruby,1.0.4,MIT
+connect,3.5.0,MIT
+connect-history-api-fallback,1.3.0,MIT
+connection_pool,2.2.1,MIT
+console-browserify,1.1.0,MIT
+console-control-strings,1.1.0,ISC
+constants-browserify,1.0.0,MIT
+contains-path,0.1.0,MIT
+content-disposition,0.5.2,MIT
+content-type,1.0.2,MIT
+convert-source-map,1.3.0,MIT
+cookie,0.3.1,MIT
+cookie-signature,1.0.6,MIT
+core-js,2.4.1,MIT
+core-util-is,1.0.2,MIT
+crack,0.4.3,MIT
+create-ecdh,4.0.0,MIT
+create-hash,1.1.2,MIT
+create-hmac,1.1.4,MIT
+creole,0.5.0,ruby
+cryptiles,2.0.5,New BSD
+crypto-browserify,3.11.0,MIT
+css_parser,1.4.1,MIT
+custom-event,1.0.1,MIT
+d,0.1.1,MIT
+d3,3.5.11,New BSD
+d3_rails,3.5.11,MIT
+dashdash,1.14.1,MIT
+date-now,0.1.4,MIT
+debug,2.6.0,MIT
+decamelize,1.2.0,MIT
+deckar01-task_list,1.0.6,MIT
+deep-extend,0.4.1,MIT
+deep-is,0.1.3,MIT
+default-require-extensions,1.0.0,MIT
+default_value_for,3.0.2,MIT
+defaults,1.0.3,MIT
+del,2.2.2,MIT
+delayed-stream,1.0.0,MIT
+delegates,1.0.0,MIT
+depd,1.1.0,MIT
+des.js,1.0.0,MIT
+descendants_tracker,0.0.4,MIT
+destroy,1.0.4,MIT
+detect-indent,4.0.0,MIT
+devise,4.2.0,MIT
+devise-two-factor,3.0.0,MIT
+di,0.0.1,MIT
+diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2"
+diffie-hellman,5.0.2,MIT
+diffy,3.1.0,MIT
+doctrine,1.5.0,BSD
+document-register-element,1.3.0,MIT
+dom-serialize,2.2.1,MIT
+domain-browser,1.1.7,MIT
+domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+doorkeeper,4.2.0,MIT
+doorkeeper-openid_connect,1.1.2,MIT
+dropzone,4.2.0,MIT
+dropzonejs-rails,0.7.2,MIT
+duplexer,0.1.1,MIT
+ecc-jsbn,0.1.1,MIT
+ee-first,1.1.1,MIT
+ejs,2.5.6,Apache 2.0
+elliptic,6.3.3,MIT
+email_reply_trimmer,0.1.6,MIT
+emoji-unicode-version,0.2.1,MIT
+emojis-list,2.1.0,MIT
+encodeurl,1.0.1,MIT
+encryptor,3.0.0,MIT
+engine.io,1.8.2,MIT
+engine.io-client,1.8.2,MIT
+engine.io-parser,1.3.2,MIT
+enhanced-resolve,3.1.0,MIT
+ent,2.2.0,MIT
+equalizer,0.0.11,MIT
+errno,0.1.4,MIT
+error-ex,1.3.0,MIT
+erubis,2.7.0,MIT
+es5-ext,0.10.12,MIT
+es6-iterator,2.0.0,MIT
+es6-map,0.1.4,MIT
+es6-promise,4.0.5,MIT
+es6-set,0.1.4,MIT
+es6-symbol,3.1.0,MIT
+es6-weak-map,2.0.1,MIT
+escape-html,1.0.3,MIT
+escape-string-regexp,1.0.5,MIT
+escape_utils,1.1.1,MIT
+escodegen,1.8.1,Simplified BSD
+escope,3.6.0,Simplified BSD
+eslint,3.15.0,MIT
+eslint-config-airbnb-base,10.0.1,MIT
+eslint-import-resolver-node,0.2.3,MIT
+eslint-import-resolver-webpack,0.8.1,MIT
+eslint-module-utils,2.0.0,MIT
+eslint-plugin-filenames,1.1.0,MIT
+eslint-plugin-import,2.2.0,MIT
+eslint-plugin-jasmine,2.2.0,MIT
+espree,3.4.0,Simplified BSD
+esprima,3.1.3,Simplified BSD
+esrecurse,4.1.0,Simplified BSD
+estraverse,4.1.1,Simplified BSD
+esutils,2.0.2,BSD
+etag,1.7.0,MIT
+eve-raphael,0.5.0,Apache 2.0
+event-emitter,0.3.4,MIT
+eventemitter3,1.2.0,MIT
+events,1.1.1,MIT
+eventsource,0.1.6,MIT
+evp_bytestokey,1.0.0,MIT
+excon,0.52.0,MIT
+execjs,2.6.0,MIT
+exit-hook,1.1.1,MIT
+expand-braces,0.1.2,MIT
+expand-brackets,0.1.5,MIT
+expand-range,1.8.2,MIT
+express,4.14.1,MIT
+expression_parser,0.9.0,MIT
+extend,3.0.0,MIT
+extglob,0.3.2,MIT
+extlib,0.9.16,MIT
+extract-zip,1.5.0,Simplified BSD
+extsprintf,1.0.2,MIT
+faraday,0.9.2,MIT
+faraday_middleware,0.10.0,MIT
+faraday_middleware-multi_json,0.0.6,MIT
+fast-levenshtein,2.0.6,MIT
+faye-websocket,0.10.0,MIT
+fd-slicer,1.0.1,MIT
+ffi,1.9.10,BSD
+figures,1.7.0,MIT
+file-entry-cache,2.0.0,MIT
+filename-regex,2.0.0,MIT
+fileset,2.0.3,MIT
+filesize,3.5.4,New BSD
+fill-range,2.2.3,MIT
+finalhandler,0.5.1,MIT
+find-cache-dir,0.1.1,MIT
+find-root,0.1.2,MIT
+find-up,2.1.0,MIT
+flat-cache,1.2.2,MIT
+flowdock,0.7.1,MIT
+fog-aws,0.11.0,MIT
+fog-core,1.42.0,MIT
+fog-google,0.5.0,MIT
+fog-json,1.0.2,MIT
+fog-local,0.3.0,MIT
+fog-openstack,0.1.6,MIT
+fog-rackspace,0.1.1,MIT
+fog-xml,0.1.2,MIT
+font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
+for-in,0.1.6,MIT
+for-own,0.1.4,MIT
+forever-agent,0.6.1,Apache 2.0
+form-data,2.1.2,MIT
+formatador,0.2.5,MIT
+forwarded,0.1.0,MIT
+fresh,0.3.0,MIT
+fs-extra,1.0.0,MIT
+fs.realpath,1.0.0,ISC
+fsevents,,unknown
+fstream,1.0.10,ISC
+fstream-ignore,1.0.5,ISC
+function-bind,1.1.0,MIT
+gauge,2.7.2,ISC
+gemnasium-gitlab-service,0.2.6,MIT
+gemojione,3.0.1,MIT
+generate-function,2.0.0,MIT
+generate-object-property,1.2.0,MIT
+get-caller-file,1.0.2,ISC
+get_process_mem,0.2.0,MIT
+getpass,0.1.6,MIT
+gitaly,0.2.1,MIT
+github-linguist,4.7.6,MIT
+github-markup,1.4.0,MIT
+gitlab-flowdock-git-hook,1.0.1,MIT
+gitlab-grit,2.8.1,MIT
+gitlab-markup,1.5.1,MIT
+gitlab_omniauth-ldap,1.2.1,MIT
+glob,7.1.1,ISC
+glob-base,0.3.0,MIT
+glob-parent,2.0.0,ISC
+globalid,0.3.7,MIT
+globals,9.14.0,MIT
+globby,5.0.0,MIT
+gollum-grit_adapter,1.0.1,MIT
+gollum-lib,4.2.1,MIT
+gollum-rugged_adapter,0.4.2,MIT
+gon,6.1.0,MIT
+google-api-client,0.8.7,Apache 2.0
+google-protobuf,3.2.0,New BSD
+googleauth,0.5.1,Apache 2.0
+graceful-fs,4.1.11,ISC
+graceful-readlink,1.0.1,MIT
+grape,0.19.1,MIT
+grape-entity,0.6.0,MIT
+grpc,1.1.2,New BSD
+gzip-size,3.0.0,MIT
+hamlit,2.6.1,MIT
+handle-thing,1.2.5,MIT
+handlebars,4.0.6,MIT
+har-validator,2.0.6,ISC
+has,1.0.1,MIT
+has-ansi,2.0.0,MIT
+has-binary,0.1.7,MIT
+has-cors,1.1.0,MIT
+has-flag,1.0.0,MIT
+has-unicode,2.0.1,ISC
+hash.js,1.0.3,MIT
+hasha,2.2.0,MIT
+hashie,3.5.5,MIT
+hawk,3.1.3,New BSD
+health_check,2.6.0,MIT
+hipchat,1.5.2,MIT
+hoek,2.16.3,New BSD
+home-or-tmp,2.0.0,MIT
+hosted-git-info,2.2.0,ISC
+hpack.js,2.1.6,MIT
+html-entities,1.2.0,MIT
+html-pipeline,1.11.0,MIT
+html2text,0.2.0,MIT
+htmlentities,4.3.4,MIT
+http,0.9.8,MIT
+http-cookie,1.0.3,MIT
+http-deceiver,1.2.7,MIT
+http-errors,1.5.1,MIT
+http-form_data,1.0.1,MIT
+http-proxy,1.16.2,MIT
+http-proxy-middleware,0.17.3,MIT
+http-signature,1.1.1,MIT
+http_parser.rb,0.6.0,MIT
+httparty,0.13.7,MIT
+httpclient,2.8.2,ruby
+https-browserify,0.0.1,MIT
+i18n,0.8.1,MIT
+ice_nine,0.11.2,MIT
+iconv-lite,0.4.15,MIT
+ieee754,1.1.8,New BSD
+ignore,3.2.2,MIT
+imurmurhash,0.1.4,MIT
+indexof,0.0.1,unknown
+inflight,1.0.6,ISC
+influxdb,0.2.3,MIT
+inherits,2.0.3,ISC
+ini,1.3.4,ISC
+inquirer,0.12.0,MIT
+interpret,1.0.1,MIT
+invariant,2.2.2,New BSD
+invert-kv,1.0.0,MIT
+ipaddr.js,1.2.0,MIT
+ipaddress,0.8.3,MIT
+is-absolute,0.2.6,MIT
+is-arrayish,0.2.1,MIT
+is-binary-path,1.0.1,MIT
+is-buffer,1.1.4,MIT
+is-builtin-module,1.0.0,MIT
+is-dotfile,1.0.2,MIT
+is-equal-shallow,0.1.3,MIT
+is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
+is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
+is-glob,2.0.1,MIT
+is-my-json-valid,2.15.0,MIT
+is-number,2.1.0,MIT
+is-path-cwd,1.0.0,MIT
+is-path-in-cwd,1.0.0,MIT
+is-path-inside,1.0.0,MIT
+is-posix-bracket,0.1.1,MIT
+is-primitive,2.0.0,MIT
+is-property,1.0.2,MIT
+is-relative,0.2.1,MIT
+is-resolvable,1.0.0,MIT
+is-stream,1.1.0,MIT
+is-typedarray,1.0.0,MIT
+is-unc-path,0.1.2,MIT
+is-utf8,0.2.1,MIT
+is-windows,0.2.0,MIT
+isarray,1.0.0,MIT
+isbinaryfile,3.0.2,MIT
+isexe,1.1.2,ISC
+isobject,2.1.0,MIT
+isstream,0.1.2,MIT
+istanbul,0.4.5,New BSD
+istanbul-api,1.1.1,New BSD
+istanbul-lib-coverage,1.0.1,New BSD
+istanbul-lib-hook,1.0.0,New BSD
+istanbul-lib-instrument,1.4.2,New BSD
+istanbul-lib-report,1.0.0-alpha.3,New BSD
+istanbul-lib-source-maps,1.1.0,New BSD
+istanbul-reports,1.0.1,New BSD
+jasmine-core,2.5.2,MIT
+jasmine-jquery,2.1.1,MIT
+jira-ruby,1.1.2,MIT
+jodid25519,1.0.2,MIT
+jquery,2.2.1,MIT
+jquery-atwho-rails,1.3.2,MIT
+jquery-rails,4.1.1,MIT
+jquery-ujs,1.2.1,MIT
+js-cookie,2.1.3,MIT
+js-tokens,3.0.1,MIT
+js-yaml,3.8.1,MIT
+jsbn,0.1.0,BSD
+jsesc,1.3.0,MIT
+json,1.8.6,ruby
+json-jwt,1.7.1,MIT
+json-loader,0.5.4,MIT
+json-schema,0.2.3,"AFLv2.1,BSD"
+json-stable-stringify,1.0.1,MIT
+json-stringify-safe,5.0.1,ISC
+json3,3.3.2,MIT
+json5,0.5.1,MIT
+jsonfile,2.4.0,MIT
+jsonify,0.0.0,Public Domain
+jsonpointer,4.0.1,MIT
+jsprim,1.3.1,MIT
+jwt,1.5.6,MIT
+kaminari,0.17.0,MIT
+karma,1.4.1,MIT
+karma-coverage-istanbul-reporter,0.2.0,MIT
+karma-jasmine,1.1.0,MIT
+karma-mocha-reporter,2.2.2,MIT
+karma-phantomjs-launcher,1.0.2,MIT
+karma-sourcemap-loader,0.3.7,MIT
+karma-webpack,2.0.2,MIT
+kew,0.7.0,Apache 2.0
+kgio,2.10.0,LGPL-2.1+
+kind-of,3.1.0,MIT
+klaw,1.3.1,MIT
+kubeclient,2.2.0,MIT
+launchy,2.4.3,ISC
+lazy-cache,1.0.4,MIT
+lcid,1.0.0,MIT
+levn,0.3.0,MIT
+licensee,8.7.0,MIT
+little-plugger,1.1.4,MIT
+load-json-file,1.1.0,MIT
+loader-runner,2.3.0,MIT
+loader-utils,0.2.16,MIT
+locate-path,2.0.0,MIT
+lodash,4.17.4,MIT
+lodash._baseget,3.7.2,MIT
+lodash._topath,3.8.1,MIT
+lodash.camelcase,4.1.1,MIT
+lodash.capitalize,4.2.1,MIT
+lodash.cond,4.5.2,MIT
+lodash.deburr,4.1.0,MIT
+lodash.get,3.7.0,MIT
+lodash.isarray,3.0.4,MIT
+lodash.kebabcase,4.0.1,MIT
+lodash.snakecase,4.0.1,MIT
+lodash.words,4.2.0,MIT
+log4js,0.6.38,Apache 2.0
+logging,2.1.0,MIT
+longest,1.0.1,MIT
+loofah,2.0.3,MIT
+loose-envify,1.3.1,MIT
+lru-cache,2.2.4,MIT
+mail,2.6.4,MIT
+mail_room,0.9.1,MIT
+media-typer,0.3.0,MIT
+memoist,0.15.0,MIT
+memory-fs,0.4.1,MIT
+merge-descriptors,1.0.1,MIT
+method_source,0.8.2,MIT
+methods,1.1.2,MIT
+micromatch,2.3.11,MIT
+miller-rabin,4.0.0,MIT
+mime,1.3.4,MIT
+mime-db,1.26.0,MIT
+mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
+mimemagic,0.3.0,MIT
+mini_portile2,2.1.0,MIT
+minimalistic-assert,1.0.0,ISC
+minimatch,3.0.3,ISC
+minimist,0.0.8,MIT
+mkdirp,0.5.1,MIT
+moment,2.17.1,MIT
+mousetrap,1.4.6,Apache 2.0
+mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.2,MIT
+multi_json,1.12.1,MIT
+multi_xml,0.6.0,MIT
+multipart-post,2.0.0,MIT
+mustermann,0.4.0,MIT
+mustermann-grape,0.4.0,MIT
+mute-stream,0.0.5,ISC
+nan,2.5.1,MIT
+natural-compare,1.4.0,MIT
+negotiator,0.6.1,MIT
+net-ldap,0.12.1,MIT
+net-ssh,3.0.1,MIT
+netrc,0.11.0,MIT
+node-libs-browser,2.0.0,MIT
+node-pre-gyp,0.6.33,New BSD
+node-zopfli,2.0.2,MIT
+nokogiri,1.6.8.1,MIT
+nopt,3.0.6,ISC
+normalize-package-data,2.3.5,Simplified BSD
+normalize-path,2.0.1,MIT
+npmlog,4.0.2,ISC
+number-is-nan,1.0.1,MIT
+numerizer,0.1.1,MIT
+oauth,0.5.1,MIT
+oauth-sign,0.8.2,Apache 2.0
+oauth2,1.2.0,MIT
+object-assign,4.1.1,MIT
+object-component,0.0.3,unknown
+object.omit,2.0.1,MIT
+obuf,1.1.1,MIT
+octokit,4.6.2,MIT
+oj,2.17.4,MIT
+omniauth,1.4.2,MIT
+omniauth-auth0,1.4.1,MIT
+omniauth-authentiq,0.3.0,MIT
+omniauth-azure-oauth2,0.0.6,MIT
+omniauth-cas3,1.1.3,MIT
+omniauth-facebook,4.0.0,MIT
+omniauth-github,1.1.2,MIT
+omniauth-gitlab,1.0.2,MIT
+omniauth-google-oauth2,0.4.1,MIT
+omniauth-kerberos,0.3.0,MIT
+omniauth-multipassword,0.4.2,MIT
+omniauth-oauth,1.1.0,MIT
+omniauth-oauth2,1.3.1,MIT
+omniauth-oauth2-generic,0.2.2,MIT
+omniauth-saml,1.7.0,MIT
+omniauth-shibboleth,1.2.1,MIT
+omniauth-twitter,1.2.1,MIT
+omniauth_crowd,2.2.3,MIT
+on-finished,2.3.0,MIT
+on-headers,1.0.1,MIT
+once,1.3.3,ISC
+onetime,1.1.0,MIT
+opener,1.4.3,(WTFPL OR MIT)
+opn,4.0.2,MIT
+optimist,0.6.1,MIT/X11
+optionator,0.8.2,MIT
+options,0.0.6,MIT
+org-ruby,0.9.12,MIT
+original,1.0.0,MIT
+orm_adapter,0.5.0,MIT
+os,0.9.6,MIT
+os-browserify,0.2.1,MIT
+os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
+os-tmpdir,1.0.2,MIT
+p-limit,1.1.0,MIT
+p-locate,2.0.0,MIT
+pako,0.2.9,MIT
+paranoia,2.2.0,MIT
+parse-asn1,5.0.0,ISC
+parse-glob,3.0.4,MIT
+parse-json,2.2.0,MIT
+parsejson,0.0.3,MIT
+parseqs,0.0.5,MIT
+parseuri,0.0.5,MIT
+parseurl,1.3.1,MIT
+path-browserify,0.0.0,MIT
+path-exists,3.0.0,MIT
+path-is-absolute,1.0.1,MIT
+path-is-inside,1.0.2,(WTFPL OR MIT)
+path-parse,1.0.5,MIT
+path-to-regexp,0.1.7,MIT
+path-type,1.1.0,MIT
+pbkdf2,3.0.9,MIT
+pend,1.2.0,MIT
+pg,0.18.4,"BSD,ruby,GPL"
+phantomjs-prebuilt,2.1.14,Apache 2.0
+pify,2.3.0,MIT
+pikaday,1.5.1,"BSD,MIT"
+pinkie,2.0.4,MIT
+pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
+pkg-up,1.0.0,MIT
+pluralize,1.2.1,MIT
+portfinder,1.0.13,MIT
+posix-spawn,0.3.11,"MIT,LGPL"
+prelude-ls,1.1.2,MIT
+premailer,1.8.6,New BSD
+premailer-rails,1.9.2,MIT
+preserve,0.2.0,MIT
+private,0.1.7,MIT
+process,0.11.9,MIT
+process-nextick-args,1.0.7,MIT
+progress,1.1.8,MIT
+proxy-addr,1.1.3,MIT
+prr,0.0.0,MIT
+public-encrypt,4.0.0,MIT
+punycode,1.4.1,MIT
+pyu-ruby-sasl,0.0.3.3,MIT
+qjobs,1.1.5,MIT
+qs,6.2.0,New BSD
+querystring,0.2.0,MIT
+querystring-es3,0.2.1,MIT
+querystringify,0.0.4,MIT
+rack,1.6.5,MIT
+rack-accept,0.4.5,MIT
+rack-attack,4.4.1,MIT
+rack-cors,0.4.0,MIT
+rack-oauth2,1.2.3,MIT
+rack-protection,1.5.3,MIT
+rack-proxy,0.6.0,MIT
+rack-test,0.6.3,MIT
+rails,4.2.8,MIT
+rails-deprecated_sanitizer,1.0.3,MIT
+rails-dom-testing,1.0.8,MIT
+rails-html-sanitizer,1.0.3,MIT
+railties,4.2.8,MIT
+rainbow,2.1.0,MIT
+raindrops,0.17.0,LGPL-2.1+
+rake,10.5.0,MIT
+randomatic,1.1.6,MIT
+randombytes,2.0.3,MIT
+range-parser,1.2.0,MIT
+raphael,2.2.7,MIT
+raw-body,2.2.0,MIT
+raw-loader,0.5.1,MIT
+rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0)
+rdoc,4.2.2,ruby
+read-pkg,1.1.0,MIT
+read-pkg-up,1.0.1,MIT
+readable-stream,2.1.5,MIT
+readdirp,2.1.0,MIT
+readline2,1.0.1,MIT
+recaptcha,3.0.0,MIT
+rechoir,0.6.2,MIT
+recursive-open-struct,1.0.0,MIT
+redcarpet,3.4.0,MIT
+redis,3.2.2,MIT
+redis-actionpack,5.0.1,MIT
+redis-activesupport,5.0.1,MIT
+redis-namespace,1.5.2,MIT
+redis-rack,1.6.0,MIT
+redis-rails,5.0.1,MIT
+redis-store,1.2.0,MIT
+regenerate,1.3.2,MIT
+regenerator-runtime,0.10.1,MIT
+regenerator-transform,0.9.8,BSD
+regex-cache,0.4.3,MIT
+regexpu-core,2.0.0,MIT
+regjsgen,0.2.0,MIT
+regjsparser,0.1.5,BSD
+repeat-element,1.1.2,MIT
+repeat-string,1.6.1,MIT
+repeating,2.0.1,MIT
+request,2.79.0,Apache 2.0
+request-progress,2.0.1,MIT
+request_store,1.3.1,MIT
+require-directory,2.1.1,MIT
+require-main-filename,1.0.1,ISC
+require-uncached,1.0.3,MIT
+requires-port,1.0.0,MIT
+resolve,1.2.0,MIT
+resolve-from,1.0.1,MIT
+responders,2.3.0,MIT
+rest-client,2.0.0,MIT
+restore-cursor,1.0.1,MIT
+retriable,1.4.1,MIT
+right-align,0.1.3,MIT
+rimraf,2.5.4,ISC
+rinku,2.0.0,ISC
+ripemd160,1.0.1,New BSD
+rotp,2.1.2,MIT
+rouge,2.0.7,MIT
+rqrcode,0.7.0,MIT
+rqrcode-rails3,0.1.7,MIT
+ruby-fogbugz,0.2.1,MIT
+ruby-prof,0.16.2,Simplified BSD
+ruby-saml,1.4.1,MIT
+rubyntlm,0.5.2,MIT
+rubypants,0.2.0,BSD
+rufus-scheduler,3.1.10,MIT
+rugged,0.24.0,MIT
+run-async,0.1.0,MIT
+rx-lite,3.1.2,Apache 2.0
+safe-buffer,5.0.1,MIT
+safe_yaml,1.0.4,MIT
+sanitize,2.1.0,MIT
+sass,3.4.22,MIT
+sass-rails,5.0.6,MIT
+sawyer,0.8.1,MIT
+securecompare,1.0.0,MIT
+seed-fu,2.3.6,MIT
+select-hose,2.0.0,MIT
+select2,3.5.2-browserify,unknown
+select2-rails,3.5.9.3,MIT
+semver,5.3.0,ISC
+send,0.14.2,MIT
+sentry-raven,2.0.2,Apache 2.0
+serve-index,1.8.0,MIT
+serve-static,1.11.2,MIT
+set-blocking,2.0.0,ISC
+set-immediate-shim,1.0.1,MIT
+setimmediate,1.0.5,MIT
+setprototypeof,1.0.2,ISC
+settingslogic,2.0.9,MIT
+sha.js,2.4.8,MIT
+shelljs,0.7.6,New BSD
+sidekiq,4.2.7,LGPL
+sidekiq-cron,0.4.4,MIT
+sidekiq-limit_fetch,3.4.0,MIT
+signal-exit,3.0.2,ISC
+signet,0.7.3,Apache 2.0
+slack-notifier,1.5.1,MIT
+slash,1.0.0,MIT
+slice-ansi,0.0.4,MIT
+sntp,1.0.9,BSD
+socket.io,1.7.2,MIT
+socket.io-adapter,0.5.0,MIT
+socket.io-client,1.7.2,MIT
+socket.io-parser,2.3.1,MIT
+sockjs,0.3.18,MIT
+sockjs-client,1.1.1,MIT
+source-list-map,0.1.8,MIT
+source-map,0.5.6,New BSD
+source-map-support,0.4.11,MIT
+spdx-correct,1.0.2,Apache 2.0
+spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
+spdx-license-ids,1.2.2,Unlicense
+spdy,3.4.4,MIT
+spdy-transport,2.0.18,MIT
+sprintf-js,1.0.3,New BSD
+sprockets,3.7.1,MIT
+sprockets-rails,3.2.0,MIT
+sshpk,1.10.2,MIT
+state_machines,0.4.0,MIT
+state_machines-activemodel,0.4.0,MIT
+state_machines-activerecord,0.4.0,MIT
+stats-webpack-plugin,0.4.3,MIT
+statuses,1.3.1,MIT
+stream-browserify,2.0.1,MIT
+stream-http,2.6.3,MIT
+string-width,1.0.2,MIT
+string.fromcodepoint,0.2.1,MIT
+string.prototype.codepointat,0.2.0,MIT
+string_decoder,0.10.31,MIT
+stringex,2.5.2,MIT
+stringstream,0.0.5,MIT
+strip-ansi,3.0.1,MIT
+strip-bom,2.0.0,MIT
+strip-json-comments,1.0.4,MIT
+supports-color,0.2.0,MIT
+sys-filesystem,1.1.6,Artistic 2.0
+table,3.8.3,New BSD
+tapable,0.2.6,MIT
+tar,2.2.1,ISC
+tar-pack,3.3.0,Simplified BSD
+temple,0.7.7,MIT
+test-exclude,4.0.0,ISC
+text-table,0.2.0,MIT
+thor,0.19.4,MIT
+thread_safe,0.3.6,Apache 2.0
+throttleit,1.0.0,MIT
+through,2.3.8,MIT
+tilt,2.0.6,MIT
+timeago.js,2.0.5,MIT
+timers-browserify,2.0.2,MIT
+timfel-krb5-auth,0.8.3,LGPL
+tmp,0.0.28,MIT
+to-array,0.1.4,MIT
+to-arraybuffer,1.0.1,MIT
+to-fast-properties,1.0.2,MIT
+tool,0.2.3,MIT
+tough-cookie,2.3.2,New BSD
+trim-right,1.0.1,MIT
+truncato,0.7.8,MIT
+tryit,1.0.3,MIT
+tty-browserify,0.0.0,MIT
+tunnel-agent,0.4.3,Apache 2.0
+tweetnacl,0.14.5,Unlicense
+type-check,0.3.2,MIT
+type-is,1.6.14,MIT
+typedarray,0.0.6,MIT
+tzinfo,1.2.2,MIT
+u2f,0.2.1,MIT
+uglifier,2.7.2,MIT
+uglify-js,2.7.5,Simplified BSD
+uglify-to-browserify,1.0.2,MIT
+uid-number,0.0.6,ISC
+ultron,1.0.2,MIT
+unc-path-regex,0.1.2,MIT
+underscore,1.8.3,MIT
+underscore-rails,1.8.3,MIT
+unf,0.1.4,BSD
+unf_ext,0.0.7.2,MIT
+unicorn,5.1.0,ruby
+unicorn-worker-killer,0.4.4,ruby
+unpipe,1.0.0,MIT
+url,0.11.0,MIT
+url-parse,1.0.5,MIT
+url_safe_base64,0.2.2,MIT
+user-home,2.0.0,MIT
+useragent,2.1.12,MIT
+util,0.10.3,MIT
+util-deprecate,1.0.2,MIT
+utils-merge,1.0.0,MIT
+uuid,3.0.1,MIT
+validate-npm-package-license,3.0.1,Apache 2.0
+validates_hostname,1.0.6,MIT
+vary,1.1.0,MIT
+verror,1.3.6,MIT
+version_sorter,2.1.0,MIT
+virtus,1.0.5,MIT
+vm-browserify,0.0.4,MIT
+vmstat,2.3.0,MIT
+void-elements,2.0.1,MIT
+vue,2.1.10,MIT
+vue-resource,0.9.3,MIT
+warden,1.2.6,MIT
+watchpack,1.2.1,MIT
+wbuf,1.7.2,MIT
+webpack,2.2.1,MIT
+webpack-bundle-analyzer,2.3.0,MIT
+webpack-dev-middleware,1.10.0,MIT
+webpack-dev-server,2.3.0,MIT
+webpack-rails,0.9.9,MIT
+webpack-sources,0.1.4,MIT
+websocket-driver,0.6.5,MIT
+websocket-extensions,0.1.1,MIT
+which,1.2.12,ISC
+which-module,1.0.0,ISC
+wide-align,1.1.0,ISC
+wikicloth,0.8.1,MIT
+window-size,0.1.0,MIT
+wordwrap,0.0.2,MIT/X11
+wrap-ansi,2.1.0,MIT
+wrappy,1.0.2,ISC
+write,0.2.1,MIT
+ws,1.1.1,MIT
+wtf-8,1.0.0,MIT
+xmlhttprequest-ssl,1.5.3,MIT
+xtend,4.0.1,MIT
+y18n,3.2.1,ISC
+yargs,3.10.0,MIT
+yargs-parser,4.2.1,ISC
+yauzl,2.4.1,MIT
+yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index ad4b5223d60dfe964ede15c55c26a0d26729845d..2500ddc6f6b8ca40ee1bdd99fd0d1f9e76a6f0bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25,7 +25,7 @@ acorn-jsx@^3.0.0:
   dependencies:
     acorn "^3.0.4"
 
-acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4:
+acorn@4.0.4, acorn@^4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
 
@@ -33,6 +33,10 @@ acorn@^3.0.4:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
+acorn@^4.0.11, acorn@^4.0.3:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+
 after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -469,6 +473,13 @@ babel-plugin-transform-decorators@^6.22.0:
     babel-template "^6.22.0"
     babel-types "^6.22.0"
 
+babel-plugin-transform-define@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+  dependencies:
+    lodash.get "4.4.2"
+    traverse "0.6.6"
+
 babel-plugin-transform-es2015-arrow-functions@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -545,17 +556,17 @@ babel-plugin-transform-es2015-literals@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-modules-amd@^6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21"
+babel-plugin-transform-es2015-modules-amd@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
   dependencies:
-    babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+    babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
     babel-runtime "^6.22.0"
     babel-template "^6.22.0"
 
-babel-plugin-transform-es2015-modules-commonjs@^6.22.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.23.0.tgz#cba7aa6379fb7ec99250e6d46de2973aaffa7b92"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
   dependencies:
     babel-plugin-transform-strict-mode "^6.22.0"
     babel-runtime "^6.22.0"
@@ -570,11 +581,11 @@ babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
     babel-runtime "^6.22.0"
     babel-template "^6.23.0"
 
-babel-plugin-transform-es2015-modules-umd@^6.22.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.23.0.tgz#8d284ae2e19ed8fe21d2b1b26d6e7e0fcd94f0f1"
+babel-plugin-transform-es2015-modules-umd@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
   dependencies:
-    babel-plugin-transform-es2015-modules-amd "^6.22.0"
+    babel-plugin-transform-es2015-modules-amd "^6.24.0"
     babel-runtime "^6.22.0"
     babel-template "^6.23.0"
 
@@ -665,9 +676,9 @@ babel-plugin-transform-strict-mode@^6.22.0:
     babel-runtime "^6.22.0"
     babel-types "^6.22.0"
 
-babel-preset-es2015@^6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835"
+babel-preset-es2015@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
   dependencies:
     babel-plugin-check-es2015-constants "^6.22.0"
     babel-plugin-transform-es2015-arrow-functions "^6.22.0"
@@ -680,10 +691,10 @@ babel-preset-es2015@^6.22.0:
     babel-plugin-transform-es2015-for-of "^6.22.0"
     babel-plugin-transform-es2015-function-name "^6.22.0"
     babel-plugin-transform-es2015-literals "^6.22.0"
-    babel-plugin-transform-es2015-modules-amd "^6.22.0"
-    babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+    babel-plugin-transform-es2015-modules-amd "^6.24.0"
+    babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
     babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
-    babel-plugin-transform-es2015-modules-umd "^6.22.0"
+    babel-plugin-transform-es2015-modules-umd "^6.24.0"
     babel-plugin-transform-es2015-object-super "^6.22.0"
     babel-plugin-transform-es2015-parameters "^6.22.0"
     babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
@@ -694,6 +705,27 @@ babel-preset-es2015@^6.22.0:
     babel-plugin-transform-es2015-unicode-regex "^6.22.0"
     babel-plugin-transform-regenerator "^6.22.0"
 
+babel-preset-es2016@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+  dependencies:
+    babel-plugin-transform-exponentiation-operator "^6.22.0"
+
+babel-preset-es2017@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+  dependencies:
+    babel-plugin-syntax-trailing-function-commas "^6.22.0"
+    babel-plugin-transform-async-to-generator "^6.22.0"
+
+babel-preset-latest@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+  dependencies:
+    babel-preset-es2015 "^6.24.0"
+    babel-preset-es2016 "^6.22.0"
+    babel-preset-es2017 "^6.22.0"
+
 babel-preset-stage-2@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
@@ -1209,7 +1241,7 @@ cookie@0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
 
-core-js@^2.2.0, core-js@^2.4.0:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
 
@@ -1391,6 +1423,10 @@ doctrine@1.5.0, doctrine@^1.2.2:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
+document-register-element@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
+
 dom-serialize@^2.2.0:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@@ -1408,6 +1444,10 @@ dropzone@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
 
+duplexer@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
 ecc-jsbn@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -1418,6 +1458,10 @@ ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
 
+ejs@^2.5.5:
+  version "2.5.6"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
+
 elliptic@^6.0.0:
   version "6.3.3"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
@@ -1427,6 +1471,10 @@ elliptic@^6.0.0:
     hash.js "^1.0.0"
     inherits "^2.0.1"
 
+emoji-unicode-version@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc"
+
 emojis-list@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@@ -1533,7 +1581,7 @@ es6-map@^0.1.3:
     es6-symbol "~3.1.0"
     event-emitter "~0.3.4"
 
-es6-promise@^4.0.5, es6-promise@~4.0.3:
+es6-promise@~4.0.3:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
 
@@ -1734,6 +1782,10 @@ etag@~1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
 
+eve-raphael@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
+
 event-emitter@~0.3.4:
   version "0.3.4"
   resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5"
@@ -1792,7 +1844,7 @@ expand-range@^1.8.1:
   dependencies:
     fill-range "^2.1.0"
 
-express@^4.13.3:
+express@^4.13.3, express@^4.14.1:
   version "4.14.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
   dependencies:
@@ -1893,6 +1945,10 @@ fileset@^2.0.2:
     glob "^7.0.3"
     minimatch "^3.0.3"
 
+filesize@^3.5.4:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
+
 fill-range@^2.1.0:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
@@ -2118,6 +2174,12 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
 
+gzip-size@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+  dependencies:
+    duplexer "^0.1.1"
+
 handle-thing@^1.2.4:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -2617,17 +2679,13 @@ jodid25519@^1.0.0:
   dependencies:
     jsbn "~0.1.0"
 
-"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4":
-  version "1.11.4"
-  resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a"
-
 jquery-ujs@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
   dependencies:
     jquery ">=1.8.0"
 
-jquery@^2.2.1, jquery@>=1.8.0:
+jquery@>=1.8.0, jquery@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
 
@@ -2824,7 +2882,7 @@ loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
 
-loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
+loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
   version "0.2.16"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
   dependencies:
@@ -2870,6 +2928,10 @@ lodash.deburr@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
 
+lodash.get@4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
 lodash.get@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -2903,7 +2965,7 @@ lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
@@ -3216,6 +3278,10 @@ onetime@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
 
+opener@^1.4.2:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+
 opn@4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95"
@@ -3532,6 +3598,12 @@ range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
 
+raphael@^2.2.7:
+  version "2.2.7"
+  resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.2.7.tgz#231b19141f8d086986d8faceb66f8b562ee2c810"
+  dependencies:
+    eve-raphael "0.5.0"
+
 raw-body@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
@@ -3540,6 +3612,10 @@ raw-body@~2.2.0:
     iconv-lite "0.4.15"
     unpipe "1.0.0"
 
+raw-loader@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
+
 rc@~1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
@@ -3964,7 +4040,7 @@ source-map-support@^0.4.2:
   dependencies:
     source-map "^0.5.3"
 
-source-map@0.1.x, source-map@^0.1.41:
+source-map@^0.1.41:
   version "0.1.43"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
   dependencies:
@@ -4227,6 +4303,10 @@ tough-cookie@~2.3.0:
   dependencies:
     punycode "^1.4.1"
 
+traverse@0.6.6:
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
 trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -4384,9 +4464,9 @@ vue-resource@^0.9.3:
   version "0.9.3"
   resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
 
-vue@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"
+vue@^2.1.10:
+  version "2.1.10"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
 
 watchpack@^1.2.0:
   version "1.2.1"
@@ -4402,6 +4482,21 @@ wbuf@^1.1.0, wbuf@^1.4.0:
   dependencies:
     minimalistic-assert "^1.0.0"
 
+webpack-bundle-analyzer@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4"
+  dependencies:
+    acorn "^4.0.11"
+    chalk "^1.1.3"
+    commander "^2.9.0"
+    ejs "^2.5.5"
+    express "^4.14.1"
+    filesize "^3.5.4"
+    gzip-size "^3.0.0"
+    lodash "^4.17.4"
+    mkdirp "^0.5.1"
+    opener "^1.4.2"
+
 webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8"